精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

99久久夜色精品国产亚洲| 美国黄色a级片| 99热国产在线| 国产99久久久久久免费看农村| 久久久久久久久久国产| 巨胸大乳www视频免费观看| 精品免费av一区二区三区| 亚洲精品视频自拍| 欧美日韩电影一区二区| 国产男女裸体做爰爽爽| 久久裸体视频| 欧美激情一级精品国产| 中文天堂资源在线| 都市激情亚洲欧美| 欧美日韩一区成人| 国产精品成人久久电影| jyzzz在线观看视频| 福利一区二区在线观看| 国产精品美乳一区二区免费| 懂色av.com| 亚洲天堂一区二区三区四区| 亚洲开心激情网| av地址在线观看| 在线一区视频观看| 五月婷婷激情综合| 色乱码一区二区三区熟女| 久久国产精品高清一区二区三区| 国产高清精品网站| 国产日韩在线视频| 超碰在线观看91| 日韩视频精品在线观看| 美女少妇精品视频| 国产午夜福利一区| 日韩精选在线| 欧美a级在线观看| 国产成人午夜电影网| 国产精品久久久久久av| 日本中文在线播放| 亚洲视频免费| 欧美裸体男粗大视频在线观看| 免费看日本黄色片| 亚洲人成网站77777在线观看| 日韩一级大片在线| 极品粉嫩美女露脸啪啪| 小明成人免费视频一区| 欧美性生交大片免费| 成人网站免费观看入口| 中日韩高清电影网| 亚洲欧美日韩国产成人精品影院| 欧美日韩一区二区三| 天堂av中文字幕| 成人福利在线看| 成人一区二区三区四区| 国产高潮在线观看| 国产一区二区伦理| 成人午夜激情网| 国产老妇伦国产熟女老妇视频| 麻豆高清免费国产一区| 国产精品精品一区二区三区午夜版| 91精品国产高清一区二区三密臀| 久久成人亚洲| 日本国产欧美一区二区三区| 男人日女人网站| 日精品一区二区三区| 国产精品jvid在线观看蜜臀| 波多野结衣黄色网址| 日日摸夜夜添夜夜添国产精品| 国产精品白嫩美女在线观看| 一区二区乱子伦在线播放| 日本不卡免费在线视频| 成人h片在线播放免费网站| 一级黄色片在线观看| 韩国v欧美v日本v亚洲v| 超碰在线97av| 深夜福利免费在线观看| 国产日产欧美精品一区二区三区| 色偷偷av一区二区三区乱| 91免费精品视频| 97人人澡人人爽人人模亚洲| 亚久久调教视频| 国产精品欧美日韩久久| 夜夜爽8888| 国产成人精品免费| 久久精品中文字幕一区二区三区| 牛牛澡牛牛爽一区二区| 国产精品不卡视频| www.成年人视频| 成人直播视频| 欧美日本国产一区| 国产高潮视频在线观看| 欧洲福利电影| 欧美激情精品久久久| youjizz在线视频| 国产一区二三区好的| 精品国产一区二区三区四区精华| 国产h在线观看| 一区二区欧美精品| 亚欧在线免费观看| 日韩在线观看中文字幕| 亚洲片在线观看| 91在线播放观看| 丝袜诱惑亚洲看片| 97超碰人人看人人| 可以直接在线观看的av| 伊人婷婷欧美激情| 久久精品免费网站| 狠狠久久伊人| 日韩视频永久免费观看| wwwwww国产| 国产一区二区影院| 午夜精品区一区二区三| 大菠萝精品导航| 91精品一区二区三区久久久久久 | 欧美v日韩v国产v| 永久免费看mv网站入口78| 97精品国产| 国产99视频精品免视看7| 亚洲精品福利网站| 国产精品久久久久7777按摩| 99热成人精品热久久66| 人妻换人妻a片爽麻豆| 激情视频亚洲| 国产一区二区欧美日韩| 日韩黄色a级片| 国产在线不卡一区| 天堂精品一区二区三区| 成av人片在线观看www| 91精品国产91久久久久久最新毛片| 亚洲第九十七页| 精品成人久久| 亚洲综合小说区| 日本免费视频在线观看| 色女孩综合影院| 91精彩刺激对白露脸偷拍| 国内精品美女在线观看| 亚洲r级在线观看| 日韩毛片久久久| 欧美性猛片aaaaaaa做受| 青青草视频成人| 亚洲美女少妇无套啪啪呻吟| ts人妖另类在线| 天天干在线视频论坛| 欧美电影在线免费观看| 一本色道久久88| 麻豆国产一区二区| 亚洲一区不卡在线| 精品三级在线| 精品国产一区二区三区久久久| 中文字幕+乱码+中文| 久久久久久9999| 成年人免费在线播放| 久久97久久97精品免视看秋霞| 欧美日本中文字幕| 亚洲精品久久久久avwww潮水| 亚洲天堂福利av| 国产高清av片| 一区二区三区四区在线观看国产日韩| 成人黄色中文字幕| 免费在线观看av| 91麻豆精品国产91久久久久久久久 | 91精品尤物| 欧美精品久久久久久久免费观看| 国产哺乳奶水91在线播放| 亚洲精品成a人| 欧美激情国产精品| 国产伦精品一区二区三区视频我| 久久综合网色—综合色88| 日日橹狠狠爱欧美超碰| 曰本一区二区三区视频| 国产精品亚洲自拍| 黄网站在线播放| 日韩精品一区二区在线| 日本在线小视频| 久久精品亚洲麻豆av一区二区| 欧美三级午夜理伦三级| 91麻豆精品国产91久久久平台| 成人福利在线视频| 欧洲性视频在线播放| 亚洲国产天堂久久国产91| 亚洲天堂男人av| 国产精品国产a级| 国产精品二区视频| 亚洲女同同性videoxma| 亚洲欧洲一区二区福利| 97成人在线| 日本久久久a级免费| 免费在线午夜视频| 亚洲国产高潮在线观看| 国产一级片免费在线观看| 亚洲日本一区二区| 无码人妻精品一区二区三区温州| 蜜桃av一区二区三区电影| 超碰10000| 九九亚洲视频| 91手机在线观看| 另类专区亚洲| 色综合视频一区中文字幕| 免费在线超碰| 日韩女优制服丝袜电影| 三级视频在线观看| 一区二区三区四区国产精品| 成人在线一级片| 懂色av中文一区二区三区| 日本www.色| 日韩午夜精品| 天堂av免费看| 欧美伦理在线视频| 国产专区一区二区三区| 高清不卡一区| 国产精品国产三级国产专播精品人 | 欧美经典一区| 国产成人a亚洲精品| 欧美亚洲系列| 日韩在线观看免费高清| 欧美日韩伦理片| 国产日韩欧美一区| 久久青青草原| 永久免费精品视频| 国产一区二区在线播放| 另类专区亚洲| 国产91av在线| 欧美aaaaaaa| 久久久精品在线| 国产高清美女一级毛片久久| 日韩av在线网| 好吊视频一区二区三区| 欧美精品视频www在线观看| 免费视频久久久| 亚洲成人精品在线观看| 欧美 日韩 国产 一区二区三区 | 麻豆成人小视频| 91精品国产自产在线丝袜啪 | 欧美精品亚州精品| 97电影在线看视频| 国产一区二区三区毛片| 日本护士...精品国| 亚洲第一天堂无码专区| 国模人体一区二区| 精品国产免费人成在线观看| 国产免费久久久| 在线不卡一区二区| 一级α片免费看刺激高潮视频| 日本高清无吗v一区| 一级黄色大片视频| 色婷婷久久久亚洲一区二区三区| 日韩网红少妇无码视频香港| 亚洲高清免费观看| 国产小视频在线观看免费| 亚洲一区在线观看网站| 亚洲xxxx3d动漫| 亚洲欧美福利一区二区| 卡通动漫亚洲综合| 一区二区三区91| 天堂资源在线播放| 天天影视涩香欲综合网| 国产精品一区二区三区四| 色综合久久久久| 久久午夜鲁丝片| 欧美日韩一区三区| 国产精品乱码久久久| 884aa四虎影成人精品一区| 国产精品羞羞答答在线| 精品美女一区二区三区| 四虎永久在线精品免费网址| 亚洲欧美中文字幕| www.亚洲资源| 欧美成人午夜激情视频| aaa在线播放视频| 中文字幕一区二区三区四区不卡 | 欧美福利一区| wwwjizzjizzcom| 影音先锋亚洲电影| 日韩avxxx| 美女一区二区视频| 爱情岛论坛亚洲自拍| 成人午夜精品在线| theav精尽人亡av| 国产欧美一区二区精品性色| 国产精品国产精品88| 一区二区三区日韩欧美精品| 91蜜桃视频在线观看| 欧美视频二区36p| 亚洲一级片免费看| 精品久久久久久久久久久院品网| 天天综合网在线| 日韩资源在线观看| 51漫画成人app入口| 国产精品va在线播放我和闺蜜| 四虎国产精品免费久久| 国产精品视频入口| 精品久久中文| 欧美a级免费视频| 巨乳诱惑日韩免费av| 一级黄色片国产| 99热在这里有精品免费| 少妇高潮惨叫久久久久| 亚洲自拍另类综合| 中文字幕av在线免费观看| 日韩欧美一二区| 激情小视频在线观看| 久久6精品影院| 国产成人免费| 久久久久久亚洲精品不卡4k岛国 | 国产午夜精品一区在线观看 | 91欧美日韩| 老太脱裤子让老头玩xxxxx| 美女视频一区在线观看| 一级黄色片毛片| 亚洲婷婷国产精品电影人久久| 精品免费囯产一区二区三区 | 国产精品美女久久| 久久狠狠久久| 日韩人妻精品一区二区三区| 久久国产66| 美女伦理水蜜桃4| 中文字幕制服丝袜成人av| 日本免费在线观看视频| 精品国产免费视频| av香蕉成人| 91精品久久久久久久久久久久久久 | 欧美午夜精品久久久久久蜜| 女人色偷偷aa久久天堂| 中文字幕av不卡在线| 91社区在线播放| 国产一级做a爰片在线看免费| 欧美酷刑日本凌虐凌虐| 黄色av网站在线看| 2019精品视频| 成人自拍在线| 欧美日韩午夜爽爽| 国内精品写真在线观看| 成年人在线免费看片| 日本黄色一区二区| 青青操视频在线| 91av在线免费观看视频| 老汉色老汉首页av亚洲| 日韩精品在线观看av| 国产精品一二三四区| 日本福利片在线观看| 欧美日韩国产一二三| porn亚洲| 国产一区二区丝袜| 91精品久久久久久久蜜月 | 精品网站在线| 欧美福利精品| 日日摸夜夜添夜夜添精品视频| 日本aaa视频| 色综合天天综合网天天看片| 桃花色综合影院| 日韩免费观看网站| 国产日产精品_国产精品毛片| 国产l精品国产亚洲区久久| 2022国产精品视频| 不卡av电影在线| 丝袜美腿精品国产二区| 亚洲日本中文| 91传媒免费视频| www.亚洲国产| 精品国产一区二区三区四| 亚洲色图美腿丝袜| 91伊人久久| 日本精品免费视频| 懂色av一区二区三区免费观看| 日本一本高清视频| 精品亚洲夜色av98在线观看| 美女100%一区| 一本久道久久综合狠狠爱亚洲精品| 韩国精品免费视频| 久久亚洲成人av| 亚洲男人av在线| 久久久加勒比| www.日本在线视频| 久久综合九色综合97_久久久| 日韩中文字幕高清| 久久精品亚洲精品| 粉嫩一区二区三区四区公司1| 欧美国产乱视频| 国产精品久久麻豆| 亚洲精品久久久久久久久久久| 午夜av不卡| 亚洲欧洲国产精品久久| 国产精品888| 久久免费激情视频| 日韩一区二区三区在线播放| 2021年精品国产福利在线| 欧美日韩第二页| 亚洲欧洲成人av每日更新| 亚洲欧美强伦一区二区| 日韩av第一页| 欧美激情偷拍| 欧美色图亚洲激情| 欧美一区午夜精品| 玛雅亚洲电影| www.欧美黄色| 日本一区二区三区四区| 一区二区三区视频| 久久国产福利国产秒拍| 五月天婷婷网站| 精品国产欧美一区二区三区成人| 国产厕拍一区|