精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

從Chrome源碼看瀏覽器的事件機制

系統 瀏覽器
本文從源碼角度介紹了事件的數據結構,從一個側面解綁事件介紹事件和DOM節點的聯系,然后重點分析了事件的捕獲及冒泡過程。相信看完本文,對事件的本質會有一個更透徹的理解。

在上一篇《從Chrome源碼看瀏覽器如何構建DOM樹》介紹了blink如何創建一棵DOM樹,在這一篇將介紹事件機制。

上一篇還有一個地方未提及,那就是在構建完DOM之后,瀏覽器將會觸發DOMContentLoaded事件,這個事件是在處理tokens的時候遇到EndOfFile標志符時觸發的:

 

  1. if (it->type() == HTMLToken::EndOfFile) {  
  2. // The EOF is assumed to be the last token of this bunch.  
  3. ASSERT(it + 1 == tokens->end());  
  4. // There should never be any chunks after the EOF.  
  5. ASSERT(m_speculations.isEmpty());  
  6. prepareToStopParsing();  
  7. break;  

上面代碼第1行,遇到結尾的token時,將會在第6行停止解析。這是***一個待處理的token,一般是跟在后面的一個\EOF標志符來的。

第6行的prepareToStopParsing,會在Document的finishedParseing里面生成一個事件,再調用dispatchEvent,進一步調用監聽函數:

 

  1. void Document::finishedParsing() {  
  2. dispatchEvent(Event::createBubble(EventTypeNames::DOMContentLoaded));  

這個dispatchEvent是EventTarget這個類的成員函數。在上一篇描述DOM的結點數據結構時將Node作為根結點,其實Node上面還有一個類,就是EventTarget。我們先來看一下事件的數據結構是怎么樣的:

1. 事件的數據結構

畫出事件相關的類圖:

從Chrome源碼看瀏覽器的事件機制

在最頂層的EventTarget提供了三個函數,分別是添加監聽add、刪除監聽remove、觸發監聽fire。一個典型的訪問者模式我在《Effective前端5:減少前端代碼耦合》提到了,這里重點看一下blink實際上是怎么實現的。

在Node類組合了一個EventTargetDataMap,這是一個哈希map,并且它是靜態成員變量。它的key值是當前結點Node實例的指針,value值是事件名稱和對應的listeners。如果畫一個示例圖,它的存儲是這樣的:

從Chrome源碼看瀏覽器的事件機制

如上,按照正常的思維,存放事件名稱和對應的訪問者應該是用一個哈希map,但是blink卻是用的向量vector + pair,這就導致在查找某個事件的訪問者的時候,需要循環所有已添加的事件名稱依次比較字符串值是否相等。為什么要用循環來做而不是map,這在它的源碼注釋做了說明:

 

  1. // We use HeapVector instead of HeapHashMap because  
  2. // - HeapVector is much more space efficient than HeapHashMap.  
  3. // - An EventTarget rarely has event listeners for many event types, and  
  4. // HeapVector is faster in such cases.  
  5. HeapVector>, 2> m_entries; 

意思是說使用vector比使用map更加節省空間,并且一個dom節點往往不太可能綁了太多的事件類型。這就啟示我們寫代碼要根據實際情況靈活處理。

同時還有一個比較有趣的事情,就是webkit用了一個EventTargetDataMap存放所有節點綁定的事件,它是一個static靜態成員變量,被所有Node的實例所共享,由于不同的實例的內存地址不一樣,所以它的key不一樣,就可以通過內存地址找到它綁的所有事件,即上面說的vector結構。為什么它要用一個類似于全局的變量?按照正常思維,每個Node結點綁的事件是獨立的,那應該把綁的事件作為每個Node實例獨立的數據,搞一個全局的還得用一個map作一個哈希映射。

一個可能的原因是EventTarget是作為所有DOM結點的事件目標的類,除了Node之外,還有FileReader、AudioNode等也會繼承于EventTarget,它們有另外一個EventTargetData。把所有的事件都放一起了,應該會方便統一處理。

這個時候你可能會冒出另外一個問題,這個EventTargetDataMap是什么釋放綁定的事件的,我把一個DOM結點刪了,它會自動去釋放綁定的的事件嗎?換句話說,刪除掉一個結點前需不需要先off掉它的事件?

2. DOM結點刪除與事件解綁

從源碼可以看到,Node的析構函數并沒有去釋放當前Node綁定的事件,所以它是不是不會自動釋放事件?為驗證,我們在添加綁定一個事件后、刪掉結點后分別打印這個map里面的數據,為此給Node添加一個打印的函數:

 

  1. void Node::printEventMap(){  
  2. EventTargetDataMap::iterator it = eventTargetDataMap().begin();  
  3. LOG (INFO) << "print event map: " 
  4. while(it != eventTargetDataMap().end()){  
  5. LOG(INFO) << ((Element*)it->key.get())->tagName();  
  6. ++it;  
  7.  

在上面的第5行,循環打印出所有Node結點的標簽名。

同時試驗的html如下:

  1. <p id="text">hello, world</p> 
  2. <script> 
  3.     function clickHandle(){ 
  4.         console.log("click"); 
  5.     } 
  6.     document.getElementById("text").addEventListener("click", clickHandle); 
  7.     document.getElementById("text").remove(); 
  8.     document.addEventListener("DOMContentLoaded"function(){ 
  9.         console.log("loaded"); 
  10.     });  
  11. </script> 

 

打印的結果如下:

 

  1. [21755:775:0204/181452.402843:INFO:Node.cpp(1910)] print event map:  
  2. [21755:775:0204/181452.403048:INFO:Node.cpp(1912)] “P”  
  3. [21755:775:0204/181452.404114:INFO:Node.cpp(1910)] print event map:  
  4. [21755:775:0204/181452.404287:INFO:Node.cpp(1912)] “P”  
  5. [21755:775:0204/181452.404466:INFO:Node.cpp(1912)] “#document” 

可以看到remove了p結點之后,它的事件依然存在。

我們看一下blink在remove里面做了什么:

 

  1. void Node::remove(ExceptionState& exceptionState) {  
  2. if (ContainerNode* parent = parentNode())  
  3. parent->removeChild(this, exceptionState);  

remove是后來W3C新加的api,所以在remove里面調的是老的removeChild,removeChild的關鍵代碼如下:

 

  1. Node* previousChild = child->previousSibling();  
  2. Node* nextChild = child->nextSibling();  
  3. if (nextChild)  
  4. nextChild->setPreviousSibling(previousChild);  
  5. if (previousChild)  
  6. previousChild->setNextSibling(nextChild);  
  7. if (m_firstChild == &oldChild)  
  8. setFirstChild(nextChild);  
  9. if (m_lastChild == &oldChild)  
  10. setLastChild(previousChild);  
  11. oldChild.setPreviousSibling(nullptr); 
  12. oldChild.setNextSibling(nullptr);  
  13. oldChild.setParentOrShadowHostNode(nullptr); 

前面幾行是重新設置DOM樹的結點關系,比較好理解。***面三行,把刪除掉的結點的兄弟指針和父指針置為null,注意這里并沒有把它delete掉,只是把它隔離開來。所以把它remove掉之后, 這個結點在內存里面依舊存在,你依然可以獲取它的innerText,把它重新append到body里面(但是不推薦這么做)。同時事件依然存在那個map里面。

什么時候這個節點會被真正的析構呢?發生在GC回收的時候,GC回收的時候會把DOM結點的內存釋放,并且會刪掉map里面的數據。為驗證,在啟動Chrome的時候加上參數:

  1. chromium test.html --js-flags='--expose_gc' 

這樣可以調用window.gc觸發gc回收,然后在上面的js demo代碼后面加上:

 

  1. setTimeout(function(){  
  2. //添加這個事件是為了觸發Chrome源碼里面添加的打印log  
  3. document.addEventListener("DOMContentLoaded"function(){});  
  4. setTimeout(function(){  
  5. window.gc();  
  6. document.addEventListener("DOMContentLoaded"function(){});  
  7. }, 3000);  
  8. }, 3000); 

打印的結果:

 

  1. [Node.cpp(1912)] print event map:  
  2. [Node.cpp(1914)] “P”  
  3. [Node.cpp(1914)] “#document”  
  4. [Element.cpp(186)] destroy element “p”  
  5. [Node.cpp(1912)] print event map:  
  6. [Node.cpp(1914)] “#document” 

后面三行是執行了GC回收后的結果——析構p標簽并更新存放事件的數據結構。

所以說刪掉一個DOM結點,并不需要手動去釋放它的事件。

需要注意的是DOM結點一旦存在一個引用,即使你把它remove掉了,GC也不會去回收,如下:

  1. <script> 
  2.     var p = document.getElementById("text"); 
  3.     p.remove(); 
  4.     window.gc(); 
  5. </script> 

 

執行了window.gc之后并不會去回收p的內存空間以及它的事件。因為還存在一個p的變量指向它,而如果將p置為null,如下:

  1. <script> 
  2.     var p = document.getElementById("text"); 
  3.     p.remove(); 
  4.     p = null
  5.     window.gc(); 
  6. </script> 

 

***的GC就管用了,或者p離開了作用域:

  1. <script> 
  2. !function(){ 
  3.     var p = document.getElementById("text"); 
  4.     p.remove(); 
  5. }() 
  6. window.gc(); 
  7. </script> 

 

自動銷毀,p結點沒有人引用了,能夠自動GC回收。

還有一個問題一直困擾著我,那就是監聽X按鈕的click,然后把它的父容器如彈框給刪了,這樣它自已本身也刪了,但是監聽函數還可以繼續執行,實體都沒有了,為什么綁在它身上的函數還可以繼續執行呢?通過上面的分析,應該可以找到答案:刪掉之后GC并不會立刻回收和釋放事件,因為在執行監聽函數的時候,里面有個this指針指向了該節點,并且this是只讀的,你不能把它置成null。所以只有執行完了回調函數,離開了作用域,this才會銷毀,才有可能被GC回收。

還有一種綁事件的方式,沒有討論:

3. DOM Level 0事件

就是使用dom結點的onclick、onfocus等屬性,添加事件,由于這個提得比較早,所以它的兼容性***。如下:

 

  1. function clickHandle(){  
  2. console.log("addEventListener click");  
  3.  
  4. var p = document.getElementById("text");  
  5. p.addEventListener("click", clickHandle);  
  6. p.onclick = function(){  
  7. console.log("onclick trigger");  
  8. }; 

如果點擊p標簽,將會觸發兩次,一次是addEventListener綁定的,另一次是onclick綁定的。onclick是如何綁定的呢:

 

  1. bool EventTarget::setAttributeEventListener(const AtomicString& eventType,  
  2. EventListener* listener) {  
  3. clearAttributeEventListener(eventType);  
  4. if (!listener)  
  5. return false 
  6. return addEventListener(eventType, listener, false);  

可以看到,***還是調的上面的addEventListener,只是在此之前要先clear掉上一次綁的屬性事件:

 

  1. bool EventTarget::clearAttributeEventListener(const AtomicString& eventType) {  
  2. EventListener* listener = getAttributeEventListener(eventType);  
  3. if (!listener)  
  4. return false 
  5. return removeEventListener(eventType, listener, false);  

在clear函數里面會去獲取上一次的listener,然后調removeEventListener,關鍵在于它怎么根據事件名稱eventType獲取上次listener呢:

 

  1. EventListener* EventTarget::getAttributeEventListener(  
  2. const AtomicString& eventType) {  
  3. EventListenerVector* listenerVector = getEventListeners(eventType);  
  4. if (!listenerVector)  
  5. return nullptr;  
  6. for (auto& eventListener : *listenerVector) {  
  7. EventListener* listener = eventListener.listener();  
  8. if (listener->isAttribute() /* && ... */)  
  9. return listener;  
  10.  
  11. return nullptr;  

在代碼上看很容易理解,首先獲取該DOM結點該事件名稱的所有listener做個循環,然后判斷這個listener是否為屬性事件。判斷成立,則返回。怎么判斷是否為屬性事件?那個是實例化事件的時候封裝好的了。

從上面的源代碼可以很清楚地看到onclick等屬性事件只能綁一次,并且和addEventListener的事件不沖突。

關于事件,還有一個很重要的概念,那就是事件的捕獲和冒泡。

4. 事件的捕獲和冒泡

用以下html做試驗:

  1. <div id="div-1"
  2.     <div id="div-2"
  3.         <div id="div-3">hello, world</div> 
  4.     </div> 
  5. </div> 

 

 

 

js綁事件如下:

 

  1. var div1 = document.getElementById("div-1"),  
  2. div2 = document.getElementById("div-2"),  
  3. div3 = document.getElementById("div-3");  
  4. function printInfo(event){  
  5. console.log(“eventPhase=“ + ””event.eventPhase + " " + this.id);  
  6.  
  7. div1.addEventListener("click", printInfo, true);  
  8. div2.addEventListener("click", printInfo, true);  
  9. div3.addEventListener("click", printInfo, true); 
  10. div1.addEventListener("click", printInfo);  
  11. div2.addEventListener("click", printInfo);  
  12. div3.addEventListener("click", printInfo); 

第三個參數為true,表示監聽在捕獲階段,點擊p標簽之后控制臺打印出:

 

  1. [CONSOLE] “eventPhase=1 div-1”  
  2. [CONSOLE] “eventPhase=1 div-2”  
  3. [CONSOLE] “eventPhase=2 div-3”  
  4. [CONSOLE] “eventPhase=2 div-3”  
  5. [CONSOLE] “eventPhase=3 div-2”  
  6. [CONSOLE] “eventPhase=3 div-1” 

在Event類定義里面可以找到關到eventPhase的定義:

 

  1. enum PhaseType {  
  2. kNone = 0,  
  3. kCapturingPhase = 1,  
  4. kAtTarget = 2,  
  5. kBubblingPhase = 3  
  6. }; 

1表示捕獲取階段,2表示在當前目標,3表示冒泡階段。把上面的phase轉化成文字,并把html/body/document也綁上事件,同時at-target只綁一次,那么整一個過程將是這樣的:

 

  1. “capture document”  
  2. “capture HTML”  
  3. “capture BODY”  
  4. “capture DIV#div-1”,  
  5. “capture DIV#div-2”,  
  6. at-target DIV#div-3”,  
  7. “bubbling DIV#div-2”,  
  8. “bubbling DIV#div-1”,  
  9. “bubbling BODY”  
  10. “bubbling HTML”  
  11. “bubbling document” 

從document一直捕獲到目標div3,然后再一直冒泡到document,如果在某個階段執行了:

  1. event.stopPropagation() 

那么后續的過程將不會繼續,例如在document的capture階段的click事件里面執行了上面的阻止傳播函數,那么控制臺只會打印出上面輸出的***行。

在研究blink是如何實現之前,我們先來看一下事件是怎么觸發和封裝的

5. 事件的觸發和封裝

以click事件為例,Blink在RenderViewImpl里面收到了外面的進程的消息:

 

  1. // IPC::Listener implementation ----------------------------------------------  
  2. bool RenderViewImpl::OnMessageReceived(const IPC::Message& message) {  
  3. // Have the super handle all other messages.  
  4. IPC_MESSAGE_UNHANDLED(handled = RenderWidget::OnMessageReceived(message))  

上文已提到,RenderViewImpl是頁面最基礎的一個類,當它收到IPC發來的消息時,根據消息的類型,調用相應的處理函數,由于這是一個input消息,所以它會調:

  1. IPC_MESSAGE_HANDLER(InputMsg_HandleInputEvent, OnHandleInputEvent) 

上面的IPC_MESSAGE_HANDLER其實是Blink定義的一個宏,這個宏其實就是一個switch-case里面的case。

這個處理函數又會調:

 

  1. WebInputEventResult WebViewImpl::handleInputEvent(  
  2. const WebInputEvent& inputEvent) {  
  3. switch (inputEvent.type) {  
  4. case WebInputEvent::MouseUp:  
  5. eventType = EventTypeNames::mouseup;  
  6. gestureIndicator = WTF::wrapUnique(  
  7. new UserGestureIndicator(m_mouseCaptureGestureToken.release()));  
  8. break;  
  9.  

它里面會根據輸入事件的類型如mouseup、touchstart、keybord事件等類型去調不同的函數。click是在mouseup里面處理的,接著在MouseEventManager里面創建一個MouseEvent,并調度事件,即捕獲和冒泡:

 

  1. WebInputEventResult MouseEventManager::dispatchMouseEvent(EventTarget* target, const AtomicString& mouseEventType, const PlatformMouseEvent& mouseEvent, EventTarget* relatedTarget, bool checkForListener) { 
  2. MouseEvent* event = MouseEvent::create( mouseEventType, targetNode->document().domWindow(), mouseEvent/*...*/);  
  3. DispatchEventResult dispatchResult = target->dispatchEvent(event);  
  4. return EventHandlingUtil::toWebInputEventResult(dispatchResult);  

上面代碼第2行創建MouseEvent,第3行dispatch。我們來看一下這個事件是如何層層封裝成一個MouseEvent的:

從Chrome源碼看瀏覽器的事件機制

上圖展示了從原始的msg轉化成了W3C標準的MouseEvent的過程。Blink的消息處理引擎把msg轉化成了WebInputEvent,這個event能夠直接靜態轉化成可讀的WebMouseEvent,也就是事件在底層的時候已經被封裝成帶有相關數據且可讀的事件了,上層再把它這些數據轉化成W3C規定格式的MouseEvent。

我們重點看下MouseEvent的create函數:

 

  1. MouseEvent* MouseEvent::create(const AtomicString& eventType, AbstractView* view, const PlatformMouseEvent& event, Node* relatedTarget) {  
  2. bool isMouseEnterOrLeave = eventType == EventTypeNames::mouseenter ||  
  3. eventType == EventTypeNames::mouseleave;  
  4. bool isCancelable = !isMouseEnterOrLeave;  
  5. bool isBubbling = !isMouseEnterOrLeave;  
  6. return MouseEvent::create 
  7. eventType, isBubbling, isCancelable, view, event.position().x()  
  8. /*.../*, &event);  

從代碼第五行可以看到鼠標事件的mouseenter和mouseleave是不會冒泡的。

另外,每個Event都有一個EventPath,記錄它冒泡的路徑:

從Chrome源碼看瀏覽器的事件機制

在dispatchEvent的時候,會初始化EventPath:

 

  1. void EventPath::initialize() {  
  2. if (eventPathShouldBeEmptyFor(*m_node, m_event))  
  3. return 
  4. calculatePath();  
  5. calculateAdjustedTargets();  
  6. calculateTreeOrderAndSetNearestAncestorClosedTree();  

第五行會去計算Path,而這個計算Path的核心邏輯非常簡單:

 

  1. void EventPath::calculatePath() {  
  2. // For performance and memory usage reasons we want to store the  
  3. // path using as few bytes as possible and with as few allocations  
  4. // as possible which is why we gather the data on the stack before  
  5. // storing it in a perfectly sized m_nodeEventContexts Vector.  
  6. HeapVector, 64> nodesInPath;  
  7. Node* current = m_node;  
  8. nodesInPath.push_back(current);  
  9. while (current) {  
  10. current = current->parentNode();  
  11. if (current 
  12. nodesInPath.push_back(current);  
  13.  
  14. m_nodeEventContexts.reserveCapacity(nodesInPath.size());  
  15. for (Node* nodeInPath : nodesInPath) {  
  16. m_nodeEventContexts.push_back(NodeEventContext(  
  17. nodeInPath, eventTargetRespectingTargetRules(*nodeInPath)));  
  18.  

第9行的while循環不斷地獲取當前node的父節點并把它push到一個vector里面,直到null即沒有父節點為止。***再把這個vector push到真正用來存儲成員變量。這段代碼我們又發現一個有趣的注釋,它說明了為什么不直接push到成員變量里面——因為vector變量會自動擴展本身大小,當push的時候容量不足時,會不斷地開辟內存,blink的實現是開辟一個單位元素的空間,剛好存放一個元素:

  1. ptr = expandCapacity(size() + 1, ptr); 

所以如果直接push_back到成員變量,會不斷地開辟新內存。于是它一開始就初始化了一個size為64的棧變量來存放,減少開辟內存的操作。另外有些vector自動擴充容量的實現,可能是size * 1.5或者size + 10,而不是size + 1,這種情況就會導致有多余的空間沒用到。

通過這樣的手段,就有了記錄事件冒泡路徑的EventPath。

6. 事件捕獲和冒泡的實現

上面第5點提到的MouseEventManager會調dispatchEvent,這個函數會先創建一個dispatcher,這個dispatcher實例化的時候就會去初始化上面的EventPath,然后再進行dispatch/事件調度:

 

  1. EventDispatcher dispatcher(node, &mediator->event());  
  2. DispatchEventResult dispatchResult = dispatcher.dispatch(); 

所以核心函數就是第2行調的dispatch,而這個函數最核心的3行代碼為:

 

  1. if (dispatchEventAtCapturing() == ContinueDispatching) {  
  2. if (dispatchEventAtTarget() == ContinueDispatching)  
  3. dispatchEventAtBubbling();  

(1)先執行Capturing,然后再執行AtTarget,***再Bubbling,我們來看一下Capturing函數:

 

  1. inline EventDispatchContinuation EventDispatcher::dispatchEventAtCapturing() { 
  2.  // Trigger capturing event handlers, starting at the top and working our way  
  3. // down.  
  4. //改變event的階段為冒泡  
  5. m_event->setEventPhase(Event::kCapturingPhase);  
  6. //先處理綁在window上的事件,并且如果event的m_propagationStopped被設置為true  
  7. //則返回done狀態,不再繼續傳播  
  8. if (m_event->eventPath().windowEventContext().handleLocalEvents(*m_event) &&  
  9. m_event->propagationStopped())  
  10. return DoneDispatching; 

上面做了一些初始化的工作后,循環EventPath依次觸發響應函數:

 

  1. //從EventPath***一個元素,即最頂層的父結點開始下濾  
  2. for (size_t i = m_event->eventPath().size() - 1; i > 0; --i) { 
  3. const NodeEventContext& eventContext = m_event->eventPath()[i];  
  4. //觸發事件響應函數  
  5. eventContext.handleLocalEvents(*m_event);  
  6. //如果響應函數設置了stopPropagation,則返回done  
  7. if (m_event->propagationStopped())  
  8. return DoneDispatching;  
  9.  
  10. return ContinueDispatching;  

注意上面的for循環終止條件的i是大于0,i為0則為currentTarget。而總的size為6,與我們上面demo控制臺打印一致。

(2)at-target的處理就很簡單了,取i為0的那個Node并觸發它的listeners:

 

  1. inline EventDispatchContinuation EventDispatcher::dispatchEventAtTarget() {  
  2. m_event->setEventPhase(Event::kAtTarget);  
  3. m_event->eventPath()[0].handleLocalEvents(*m_event);  
  4. return m_event->propagationStopped() ? DoneDispatching : ContinueDispatching;  

(3)bubbling的處理稍復雜,因為它還要處理cancleBubble的情況,不過總體的邏輯是類似的,核心代碼如下:

 

  1. inline void EventDispatcher::dispatchEventAtBubbling() {  
  2. // Trigger bubbling event handlers, starting at the bottom and working our way  
  3. // up.  
  4. size_t size = m_event->eventPath().size();  
  5. for (size_t i = 1; i < size; ++i) {  
  6. const NodeEventContext& eventContext = m_event->eventPath()[i];  
  7. if (m_event->bubbles() && !m_event->cancelBubble()) {  
  8. m_event->setEventPhase(Event::kBubblingPhase);  
  9.  
  10. eventContext.handleLocalEvents(*m_event);  
  11. if (m_event->propagationStopped())  
  12. return 
  13.  

可以看到bubbling的for循環是從i = 1開始,和capturing相反。因為bubble是三個階段***處理的,所以它不用再返回一個標志了。

上面介紹完了事件的捕獲和冒泡,我們注意到一個細節,所有的事件都會先在capture階段在windows上觸發。

綜合以上,本文從源碼角度介紹了事件的數據結構,從一個側面解綁事件介紹事件和DOM節點的聯系,然后重點分析了事件的捕獲及冒泡過程。相信看完本文,對事件的本質會有一個更透徹的理解。

責任編輯:未麗燕 來源: 碼農網
相關推薦

2017-02-28 10:05:56

Chrome源碼

2017-11-21 14:56:59

2017-02-07 09:44:12

Chrome源碼DOM樹

2012-07-04 17:00:06

獵豹瀏覽瀏覽器

2011-06-21 16:52:48

2020-12-23 07:37:17

瀏覽器HTML DOM0

2009-11-26 10:55:41

2017-01-05 09:07:25

JavaScript瀏覽器驅動

2010-01-28 10:13:43

2022-02-07 21:49:06

瀏覽器渲染chromium

2017-04-05 20:00:32

ChromeObjectJS代碼

2015-01-21 15:45:50

斯巴達瀏覽器

2018-02-02 15:48:47

ChromeDNS解析

2011-11-11 10:35:04

2009-12-06 09:38:02

Chrome瀏覽器Avast

2009-03-07 09:57:41

Realplayer捆綁Chrome

2009-12-03 10:56:34

谷歌Chrome瀏覽器

2016-10-09 08:38:01

JavaScript瀏覽器事件

2010-01-10 17:50:17

2009-09-22 09:17:46

谷歌Chrome瀏覽器
點贊
收藏

51CTO技術棧公眾號

日本视频一区在线观看| 欧美高清视频免费观看| 欧美激情精品久久久久久小说| 熟妇高潮一区二区三区| 亚洲激情婷婷| 亚洲男女性事视频| 欧美日韩中文不卡| a毛片毛片av永久免费| 91大神在线网站| 一区二区国产在线观看| 亚洲欧美日本精品| 99视频在线观看视频| bbw在线视频| 国产精品伦一区二区三级视频| 欧美激情亚洲综合一区| 蜜桃无码一区二区三区| 国产精品中文| 91久久国产综合久久| 不卡日韩av| 波多野结衣影片| 97久久亚洲| 欧美优质美女网站| 久久国产午夜精品理论片最新版本| 免费在线黄色网址| 国产寡妇亲子伦一区二区| 亲子乱一区二区三区电影 | 欧美人妻精品一区二区三区| 香蕉一区二区| 日韩精品一区二区三区三区免费| 少妇黄色一级片| 天堂av在线| 亚洲欧美偷拍另类a∨色屁股| 国产精品美女无圣光视频| ass精品国模裸体欣赏pics| 国产一区 二区| 欧美日韩一区二区三区在线 | 日韩精品福利网| 午夜精品久久久久久久99黑人| 天天操夜夜操av| 成人3d动漫在线观看| 精品视频在线播放| 四虎永久免费观看| 日韩区一区二| 欧美一区二区三区在线| 亚洲欧美日韩一级| 亚洲四虎影院| 日本韩国一区二区| 国产成人无码一二三区视频| 九色porny丨国产首页在线| 一区二区三区在线播| 妞干网这里只有精品| 国产日韩一级片| 亚洲色图网站| 日韩视频免费直播| 久久久久久久久久毛片| 9999在线精品视频| 7777女厕盗摄久久久| 日韩av在线中文| 久久91视频| 18成人在线观看| 资源网第一页久久久| 亚洲经典一区二区| 久久精品官网| 欧美又大粗又爽又黄大片视频| 在线天堂中文字幕| 亚洲欧美日韩国产一区| 茄子视频成人在线| 奴色虐av一区二区三区| 日本91福利区| 成人高h视频在线| 国产ts变态重口人妖hd| 国产农村妇女毛片精品久久莱园子| 国精产品一区一区三区有限在线| 免费人成视频在线| 1024成人| 欧洲亚洲免费视频| 国产乱码在线观看| 精品一区中文字幕| 99re国产在线播放| 天堂在线一二区| 国产自产高清不卡| 99视频网站| 水中色av综合| 国产精品久久久久久一区二区三区| 一区二区三区国| 丝袜综合欧美| 欧美国产综合色视频| aa成人免费视频| 亚州av在线播放| 久久久久久久网| 91免费视频黄| 国产精品vvv| 欧美性xxxxx极品少妇| 免费网站在线观看黄| 老司机精品视频在线播放| 亚洲无限av看| 欧美日韩精品一区二区三区视频播放| 日韩图片一区| 成人日韩av在线| 无码精品黑人一区二区三区| 中文字幕乱码一区二区免费| 狠狠干视频网站| 日韩成人动漫| 欧美xfplay| 18精品爽国产三级网站| 一区久久精品| 国产日产欧美a一级在线| 后进极品白嫩翘臀在线视频| 中文字幕免费不卡| 欧美 日韩 亚洲 一区| 精品女同一区二区三区在线观看| 精品国产区一区| 午夜精品一区二区三级视频| 亚洲深夜激情| 不卡一区二区三区视频| 8888四色奇米在线观看| 欧美日韩免费网站| 一级黄色免费毛片| 欧美视频免费| 欧美中文在线观看| 亚洲av无码乱码国产精品| 国产欧美一区二区三区沐欲| 亚洲熟妇无码另类久久久| 青草综合视频| 在线播放国产一区二区三区| 青青操免费在线视频| 国产福利91精品一区| 亚洲一区在线直播| 亚洲mmav| 亚洲色图国产精品| 亚洲精品色午夜无码专区日韩| 午夜久久99| 91精品视频专区| 在线日本中文字幕| 欧亚一区二区三区| 国产精品高清无码在线观看| 国产欧美一区二区色老头 | 韩国女主播一区二区三区| 欧美成人午夜剧场免费观看| 国产又大又粗又硬| 国产精品久久久久久久裸模| 亚洲一区二区蜜桃| 国产一区二区电影在线观看| 国产91精品最新在线播放| 婷婷国产在线| 91精品办公室少妇高潮对白| 老鸭窝一区二区| 欧美一级网站| 日本成人三级| 日韩在线观看不卡| 国产一区二区三区在线观看视频 | 精品国产一区二区三区四区精华 | 亚洲毛片一区| 久99久视频| 黑人巨大精品| 亚洲香蕉成人av网站在线观看| 日本a级c片免费看三区| 久久精品亚洲精品国产欧美kt∨ | 国产原创一区| 国产亚洲一区精品| 888奇米影视| 亚洲欧美日韩在线| 国产高潮失禁喷水爽到抽搐| 亚洲高清成人| 久久伊人一区二区| av成人亚洲| 日韩精品一区二区三区四区| www.超碰在线观看| 亚洲在线电影| 日韩av一区二区三区在线| 天堂资源在线中文| 欧美精品在线一区二区| 欧美 日韩 国产 一区二区三区| 国产一区二区导航在线播放| 国产免费裸体视频| 偷拍亚洲精品| 国产免费一区视频观看免费 | 欧美一级大胆视频| 成年人在线视频免费观看| 欧美精品 国产精品| 欧美精品乱码视频一二专区| 99国产精品一区| 三级在线免费观看| 超碰在线成人| 国产精品福利在线| 中文字幕在线观看播放| 日韩精品一二三四区| 又骚又黄的视频| 一区二区三区日韩| 欧美老熟妇乱大交xxxxx| 美女视频一区二区| 蜜臀av无码一区二区三区| 精品久久久亚洲| 91视频国产一区| 成人免费无遮挡| 欧美成年人视频网站| 日本私人网站在线观看| 欧美高清性hdvideosex| 国产精品视频免费播放| 中文字幕亚洲一区二区av在线 | 国产综合av在线| 久久免费av| 精品一区二区国产| 国产精品国产亚洲精品| 日本精品视频在线| 色呦呦呦在线观看| 综合网中文字幕| 亚洲av毛片成人精品| 91精品国产入口| 91丨九色丨海角社区| 亚洲国产一区二区三区| 亚洲熟女少妇一区二区| av在线综合网| 中文字幕 欧美 日韩| 蜜桃久久精品一区二区| ww国产内射精品后入国产| 欧美一区免费| 亚洲综合网中心| 久久99久久人婷婷精品综合 | 中文字幕日本一区二区| 欧美黑人一级爽快片淫片高清| 高清性色生活片在线观看| 精品成a人在线观看| 国产精品永久久久久久久久久| 色悠久久久久综合欧美99| 日本网站免费观看| 亚洲色图都市小说| 青青青手机在线视频| 久久久久久久网| 爱爱免费小视频| 成人av电影免费在线播放| 久久精品国产99久久99久久久| 日韩av在线发布| 欧美 日韩 国产 高清| 好看的av在线不卡观看| 成年丰满熟妇午夜免费视频| 日韩综合网站| 色999五月色| 国内精品伊人久久久| 青青影院一区二区三区四区| 日韩伦理一区二区三区| 精品不卡在线| 性欧美xxxx免费岛国不卡电影| 国产精品久久久久久久天堂第1集| 精品视频一二| 91福利视频导航| 久久国产精品免费一区二区三区| 国产日韩欧美综合| 日韩毛片免费看| 成人免费观看网址| 国产精品视频一区二区三区| 91视频88av| 99这里只有精品视频| 国产精品v欧美精品∨日韩| 麻豆国产一区| 成人av网站观看| 国产精品tv| 精品一区二区国产| 国产免费久久| 亚洲图片都市激情| 98精品视频| 国产又粗又大又爽的视频| 欧美jizzhd精品欧美巨大免费| 天天想你在线观看完整版电影免费| 在线国产一区二区| 国产成人一二三区| 亚洲高清在线| 99热成人精品热久久66| 奇米一区二区三区| 一区二区三区四区毛片| 国产黄色精品视频| 漂亮人妻被黑人久久精品| 91丨porny丨中文| 91视频免费在观看| 1区2区3区精品视频| 久久免费视频99| 国产福利精品在线| 一级肉体全黄裸片| 国产女同互慰高潮91漫画| 蜜乳av中文字幕| 中文字幕一区二区在线观看| 久草免费新视频| 欧美日韩一区二区三区在线免费观看| 成人h动漫精品一区二区下载| 欧美亚洲国产bt| av加勒比在线| 亚洲精品一区中文| 久操免费在线| 91av网站在线播放| 亚洲成a人片777777久久| 国产精品美女久久久久av福利| 亚洲欧洲美洲国产香蕉| 中文字幕色一区二区| 激情成人亚洲| 天堂av在线网站| 国产成人aaa| 波多野结衣 在线| 亚洲欧美日韩国产综合在线| 日本三级网站在线观看| 欧美日韩一区中文字幕| 三级小视频在线观看| 中文字幕欧美专区| av福利导福航大全在线| 国产久一一精品| 老汉色老汉首页av亚洲| 国产美女视频免费| 日韩精品一级中文字幕精品视频免费观看| 国产精品久久久久久9999| 久久亚洲综合色| 精国产品一区二区三区a片| 色婷婷综合久久久中文字幕| 亚洲av无码国产综合专区| 最近2019中文字幕大全第二页 | 国产又大又黑又粗| 亚洲精品一区av在线播放| 日本三级韩国三级欧美三级| 国产精品青草久久久久福利99| 欧美成人专区| 日本香蕉视频在线观看| 九一九一国产精品| 少妇久久久久久久久久| 欧美日韩国产在线看| 亚洲欧美另类一区| 久久视频免费在线播放| 国产成人精选| 日韩视频精品| 久久蜜桃精品| free性中国hd国语露脸| 亚洲一区二区黄色| 国产高清第一页| xxx一区二区| 欧美视频在线视频精品| 日本免费高清一区二区| 夜夜嗨一区二区| 亚洲男女在线观看| 亚洲一区欧美一区| 午夜久久久久久噜噜噜噜| 欧美成年人视频| 香港久久久电影| 日本精品福利视频| 国产经典欧美精品| 福利所第一导航| 日韩免费成人网| 成人免费一区二区三区牛牛| 成人精品一二区| 好吊日精品视频| 99久久人妻精品免费二区| 热久久国产精品| 亚洲性猛交xxxx乱大交| 色狠狠综合天天综合综合| 欧美偷拍视频| 日本中文字幕不卡免费| 国产一区二区三区探花 | 美女网站一区二区| 日韩一级片在线免费观看| 欧美视频一区在线观看| 亚乱亚乱亚洲乱妇| 成人网在线观看| 国产精品magnet| 精品一区二区三区四区五区六区| 亚洲国产中文字幕在线视频综合 | 国产精品国产亚洲精品| 亚洲小视频在线播放| 国产成人av影院| av资源免费观看| 在线观看久久av| 不卡一区视频| 我的公把我弄高潮了视频| 久久精品欧美日韩| 91成人一区二区三区| 久久成人这里只有精品| 99国产精品久久一区二区三区| 欧美丰满熟妇bbbbbb百度| 欧美激情在线一区二区三区| 国产男女裸体做爰爽爽| 97国产精品视频| 精品国产一区一区二区三亚瑟| 亚洲免费黄色录像| 亚洲午夜视频在线| 久草福利在线| 91久久精品国产| 国产日韩欧美| 娇小11一12╳yⅹ╳毛片| 精品少妇一区二区三区日产乱码| 超碰99在线| 一区二区精品在线| 成人性生交大片免费看中文网站| 成年人视频在线免费看| 久久精品小视频| 日韩av三区| 91香蕉视频在线观看视频| 都市激情亚洲色图| 男人的天堂在线视频免费观看 | 91手机在线观看| 久久国产一二区| 久久久久久久久久久久久久久久久 | 国产欧美日韩在线一区二区| 久久久久久久久久毛片| 色婷婷国产精品| 91精品久久|