從Chrome源碼看瀏覽器的事件機制
在上一篇《從Chrome源碼看瀏覽器如何構(gòu)建DOM樹》介紹了blink如何創(chuàng)建一棵DOM樹,在這一篇將介紹事件機制。
上一篇還有一個地方未提及,那就是在構(gòu)建完DOM之后,瀏覽器將會觸發(fā)DOMContentLoaded事件,這個事件是在處理tokens的時候遇到EndOfFile標志符時觸發(fā)的:
- if (it->type() == HTMLToken::EndOfFile) {
 - // The EOF is assumed to be the last token of this bunch.
 - ASSERT(it + 1 == tokens->end());
 - // There should never be any chunks after the EOF.
 - ASSERT(m_speculations.isEmpty());
 - prepareToStopParsing();
 - break;
 - }
 
上面代碼第1行,遇到結(jié)尾的token時,將會在第6行停止解析。這是***一個待處理的token,一般是跟在后面的一個\EOF標志符來的。
第6行的prepareToStopParsing,會在Document的finishedParseing里面生成一個事件,再調(diào)用dispatchEvent,進一步調(diào)用監(jiān)聽函數(shù):
- void Document::finishedParsing() {
 - dispatchEvent(Event::createBubble(EventTypeNames::DOMContentLoaded));
 - }
 
這個dispatchEvent是EventTarget這個類的成員函數(shù)。在上一篇描述DOM的結(jié)點數(shù)據(jù)結(jié)構(gòu)時將Node作為根結(jié)點,其實Node上面還有一個類,就是EventTarget。我們先來看一下事件的數(shù)據(jù)結(jié)構(gòu)是怎么樣的:
1. 事件的數(shù)據(jù)結(jié)構(gòu)
畫出事件相關的類圖:
在最頂層的EventTarget提供了三個函數(shù),分別是添加監(jiān)聽add、刪除監(jiān)聽remove、觸發(fā)監(jiān)聽fire。一個典型的訪問者模式我在《Effective前端5:減少前端代碼耦合》提到了,這里重點看一下blink實際上是怎么實現(xiàn)的。
在Node類組合了一個EventTargetDataMap,這是一個哈希map,并且它是靜態(tài)成員變量。它的key值是當前結(jié)點Node實例的指針,value值是事件名稱和對應的listeners。如果畫一個示例圖,它的存儲是這樣的:
如上,按照正常的思維,存放事件名稱和對應的訪問者應該是用一個哈希map,但是blink卻是用的向量vector + pair,這就導致在查找某個事件的訪問者的時候,需要循環(huán)所有已添加的事件名稱依次比較字符串值是否相等。為什么要用循環(huán)來做而不是map,這在它的源碼注釋做了說明:
- // We use HeapVector instead of HeapHashMap because
 - // - HeapVector is much more space efficient than HeapHashMap.
 - // - An EventTarget rarely has event listeners for many event types, and
 - // HeapVector is faster in such cases.
 - HeapVector>, 2> m_entries;
 
意思是說使用vector比使用map更加節(jié)省空間,并且一個dom節(jié)點往往不太可能綁了太多的事件類型。這就啟示我們寫代碼要根據(jù)實際情況靈活處理。
同時還有一個比較有趣的事情,就是webkit用了一個EventTargetDataMap存放所有節(jié)點綁定的事件,它是一個static靜態(tài)成員變量,被所有Node的實例所共享,由于不同的實例的內(nèi)存地址不一樣,所以它的key不一樣,就可以通過內(nèi)存地址找到它綁的所有事件,即上面說的vector結(jié)構(gòu)。為什么它要用一個類似于全局的變量?按照正常思維,每個Node結(jié)點綁的事件是獨立的,那應該把綁的事件作為每個Node實例獨立的數(shù)據(jù),搞一個全局的還得用一個map作一個哈希映射。
一個可能的原因是EventTarget是作為所有DOM結(jié)點的事件目標的類,除了Node之外,還有FileReader、AudioNode等也會繼承于EventTarget,它們有另外一個EventTargetData。把所有的事件都放一起了,應該會方便統(tǒng)一處理。
這個時候你可能會冒出另外一個問題,這個EventTargetDataMap是什么釋放綁定的事件的,我把一個DOM結(jié)點刪了,它會自動去釋放綁定的的事件嗎?換句話說,刪除掉一個結(jié)點前需不需要先off掉它的事件?
2. DOM結(jié)點刪除與事件解綁
從源碼可以看到,Node的析構(gòu)函數(shù)并沒有去釋放當前Node綁定的事件,所以它是不是不會自動釋放事件?為驗證,我們在添加綁定一個事件后、刪掉結(jié)點后分別打印這個map里面的數(shù)據(jù),為此給Node添加一個打印的函數(shù):
- void Node::printEventMap(){
 - EventTargetDataMap::iterator it = eventTargetDataMap().begin();
 - LOG (INFO) << "print event map: ";
 - while(it != eventTargetDataMap().end()){
 - LOG(INFO) << ((Element*)it->key.get())->tagName();
 - ++it;
 - }
 - }
 
在上面的第5行,循環(huán)打印出所有Node結(jié)點的標簽名。
同時試驗的html如下:
- <p id="text">hello, world</p>
 - <script>
 - function clickHandle(){
 - console.log("click");
 - }
 - document.getElementById("text").addEventListener("click", clickHandle);
 - document.getElementById("text").remove();
 - document.addEventListener("DOMContentLoaded", function(){
 - console.log("loaded");
 - });
 - </script>
 
打印的結(jié)果如下:
- [21755:775:0204/181452.402843:INFO:Node.cpp(1910)] print event map:
 - [21755:775:0204/181452.403048:INFO:Node.cpp(1912)] “P”
 - [21755:775:0204/181452.404114:INFO:Node.cpp(1910)] print event map:
 - [21755:775:0204/181452.404287:INFO:Node.cpp(1912)] “P”
 - [21755:775:0204/181452.404466:INFO:Node.cpp(1912)] “#document”
 
可以看到remove了p結(jié)點之后,它的事件依然存在。
我們看一下blink在remove里面做了什么:
- void Node::remove(ExceptionState& exceptionState) {
 - if (ContainerNode* parent = parentNode())
 - parent->removeChild(this, exceptionState);
 - }
 
remove是后來W3C新加的api,所以在remove里面調(diào)的是老的removeChild,removeChild的關鍵代碼如下:
- Node* previousChild = child->previousSibling();
 - Node* nextChild = child->nextSibling();
 - if (nextChild)
 - nextChild->setPreviousSibling(previousChild);
 - if (previousChild)
 - previousChild->setNextSibling(nextChild);
 - if (m_firstChild == &oldChild)
 - setFirstChild(nextChild);
 - if (m_lastChild == &oldChild)
 - setLastChild(previousChild);
 - oldChild.setPreviousSibling(nullptr);
 - oldChild.setNextSibling(nullptr);
 - oldChild.setParentOrShadowHostNode(nullptr);
 
前面幾行是重新設置DOM樹的結(jié)點關系,比較好理解。***面三行,把刪除掉的結(jié)點的兄弟指針和父指針置為null,注意這里并沒有把它delete掉,只是把它隔離開來。所以把它remove掉之后, 這個結(jié)點在內(nèi)存里面依舊存在,你依然可以獲取它的innerText,把它重新append到body里面(但是不推薦這么做)。同時事件依然存在那個map里面。
什么時候這個節(jié)點會被真正的析構(gòu)呢?發(fā)生在GC回收的時候,GC回收的時候會把DOM結(jié)點的內(nèi)存釋放,并且會刪掉map里面的數(shù)據(jù)。為驗證,在啟動Chrome的時候加上參數(shù):
- chromium test.html --js-flags='--expose_gc'
 
這樣可以調(diào)用window.gc觸發(fā)gc回收,然后在上面的js demo代碼后面加上:
- setTimeout(function(){
 - //添加這個事件是為了觸發(fā)Chrome源碼里面添加的打印log
 - document.addEventListener("DOMContentLoaded", function(){});
 - setTimeout(function(){
 - window.gc();
 - document.addEventListener("DOMContentLoaded", function(){});
 - }, 3000);
 - }, 3000);
 
打印的結(jié)果:
- [Node.cpp(1912)] print event map:
 - [Node.cpp(1914)] “P”
 - [Node.cpp(1914)] “#document”
 - [Element.cpp(186)] destroy element “p”
 - [Node.cpp(1912)] print event map:
 - [Node.cpp(1914)] “#document”
 
后面三行是執(zhí)行了GC回收后的結(jié)果——析構(gòu)p標簽并更新存放事件的數(shù)據(jù)結(jié)構(gòu)。
所以說刪掉一個DOM結(jié)點,并不需要手動去釋放它的事件。
需要注意的是DOM結(jié)點一旦存在一個引用,即使你把它remove掉了,GC也不會去回收,如下:
- <script>
 - var p = document.getElementById("text");
 - p.remove();
 - window.gc();
 - </script>
 
執(zhí)行了window.gc之后并不會去回收p的內(nèi)存空間以及它的事件。因為還存在一個p的變量指向它,而如果將p置為null,如下:
- <script>
 - var p = document.getElementById("text");
 - p.remove();
 - p = null;
 - window.gc();
 - </script>
 
***的GC就管用了,或者p離開了作用域:
- <script>
 - !function(){
 - var p = document.getElementById("text");
 - p.remove();
 - }()
 - window.gc();
 - </script>
 
自動銷毀,p結(jié)點沒有人引用了,能夠自動GC回收。
還有一個問題一直困擾著我,那就是監(jiān)聽X按鈕的click,然后把它的父容器如彈框給刪了,這樣它自已本身也刪了,但是監(jiān)聽函數(shù)還可以繼續(xù)執(zhí)行,實體都沒有了,為什么綁在它身上的函數(shù)還可以繼續(xù)執(zhí)行呢?通過上面的分析,應該可以找到答案:刪掉之后GC并不會立刻回收和釋放事件,因為在執(zhí)行監(jiān)聽函數(shù)的時候,里面有個this指針指向了該節(jié)點,并且this是只讀的,你不能把它置成null。所以只有執(zhí)行完了回調(diào)函數(shù),離開了作用域,this才會銷毀,才有可能被GC回收。
還有一種綁事件的方式,沒有討論:
3. DOM Level 0事件
就是使用dom結(jié)點的onclick、onfocus等屬性,添加事件,由于這個提得比較早,所以它的兼容性***。如下:
- function clickHandle(){
 - console.log("addEventListener click");
 - }
 - var p = document.getElementById("text");
 - p.addEventListener("click", clickHandle);
 - p.onclick = function(){
 - console.log("onclick trigger");
 - };
 
如果點擊p標簽,將會觸發(fā)兩次,一次是addEventListener綁定的,另一次是onclick綁定的。onclick是如何綁定的呢:
- bool EventTarget::setAttributeEventListener(const AtomicString& eventType,
 - EventListener* listener) {
 - clearAttributeEventListener(eventType);
 - if (!listener)
 - return false;
 - return addEventListener(eventType, listener, false);
 - }
 
可以看到,***還是調(diào)的上面的addEventListener,只是在此之前要先clear掉上一次綁的屬性事件:
- bool EventTarget::clearAttributeEventListener(const AtomicString& eventType) {
 - EventListener* listener = getAttributeEventListener(eventType);
 - if (!listener)
 - return false;
 - return removeEventListener(eventType, listener, false);
 - }
 
在clear函數(shù)里面會去獲取上一次的listener,然后調(diào)removeEventListener,關鍵在于它怎么根據(jù)事件名稱eventType獲取上次listener呢:
- EventListener* EventTarget::getAttributeEventListener(
 - const AtomicString& eventType) {
 - EventListenerVector* listenerVector = getEventListeners(eventType);
 - if (!listenerVector)
 - return nullptr;
 - for (auto& eventListener : *listenerVector) {
 - EventListener* listener = eventListener.listener();
 - if (listener->isAttribute() /* && ... */)
 - return listener;
 - }
 - return nullptr;
 - }
 
在代碼上看很容易理解,首先獲取該DOM結(jié)點該事件名稱的所有l(wèi)istener做個循環(huán),然后判斷這個listener是否為屬性事件。判斷成立,則返回。怎么判斷是否為屬性事件?那個是實例化事件的時候封裝好的了。
從上面的源代碼可以很清楚地看到onclick等屬性事件只能綁一次,并且和addEventListener的事件不沖突。
關于事件,還有一個很重要的概念,那就是事件的捕獲和冒泡。
4. 事件的捕獲和冒泡
用以下html做試驗:
- <div id="div-1">
 - <div id="div-2">
 - <div id="div-3">hello, world</div>
 - </div>
 - </div>
 
- var div1 = document.getElementById("div-1"),
 - div2 = document.getElementById("div-2"),
 - div3 = document.getElementById("div-3");
 - function printInfo(event){
 - console.log(“eventPhase=“ + ””event.eventPhase + " " + this.id);
 - }
 - div1.addEventListener("click", printInfo, true);
 - div2.addEventListener("click", printInfo, true);
 - div3.addEventListener("click", printInfo, true);
 - div1.addEventListener("click", printInfo);
 - div2.addEventListener("click", printInfo);
 - div3.addEventListener("click", printInfo);
 
第三個參數(shù)為true,表示監(jiān)聽在捕獲階段,點擊p標簽之后控制臺打印出:
- [CONSOLE] “eventPhase=1 div-1”
 - [CONSOLE] “eventPhase=1 div-2”
 - [CONSOLE] “eventPhase=2 div-3”
 - [CONSOLE] “eventPhase=2 div-3”
 - [CONSOLE] “eventPhase=3 div-2”
 - [CONSOLE] “eventPhase=3 div-1”
 
在Event類定義里面可以找到關到eventPhase的定義:
- enum PhaseType {
 - kNone = 0,
 - kCapturingPhase = 1,
 - kAtTarget = 2,
 - kBubblingPhase = 3
 - };
 
1表示捕獲取階段,2表示在當前目標,3表示冒泡階段。把上面的phase轉(zhuǎn)化成文字,并把html/body/document也綁上事件,同時at-target只綁一次,那么整一個過程將是這樣的:
- “capture document”
 - “capture HTML”
 - “capture BODY”
 - “capture DIV#div-1”,
 - “capture DIV#div-2”,
 - “at-target DIV#div-3”,
 - “bubbling DIV#div-2”,
 - “bubbling DIV#div-1”,
 - “bubbling BODY”
 - “bubbling HTML”
 - “bubbling document”
 
從document一直捕獲到目標div3,然后再一直冒泡到document,如果在某個階段執(zhí)行了:
- event.stopPropagation()
 
那么后續(xù)的過程將不會繼續(xù),例如在document的capture階段的click事件里面執(zhí)行了上面的阻止傳播函數(shù),那么控制臺只會打印出上面輸出的***行。
在研究blink是如何實現(xiàn)之前,我們先來看一下事件是怎么觸發(fā)和封裝的
5. 事件的觸發(fā)和封裝
以click事件為例,Blink在RenderViewImpl里面收到了外面的進程的消息:
- // IPC::Listener implementation ----------------------------------------------
 - bool RenderViewImpl::OnMessageReceived(const IPC::Message& message) {
 - // Have the super handle all other messages.
 - IPC_MESSAGE_UNHANDLED(handled = RenderWidget::OnMessageReceived(message))
 - }
 
上文已提到,RenderViewImpl是頁面最基礎的一個類,當它收到IPC發(fā)來的消息時,根據(jù)消息的類型,調(diào)用相應的處理函數(shù),由于這是一個input消息,所以它會調(diào):
- IPC_MESSAGE_HANDLER(InputMsg_HandleInputEvent, OnHandleInputEvent)
 
上面的IPC_MESSAGE_HANDLER其實是Blink定義的一個宏,這個宏其實就是一個switch-case里面的case。
這個處理函數(shù)又會調(diào):
- WebInputEventResult WebViewImpl::handleInputEvent(
 - const WebInputEvent& inputEvent) {
 - switch (inputEvent.type) {
 - case WebInputEvent::MouseUp:
 - eventType = EventTypeNames::mouseup;
 - gestureIndicator = WTF::wrapUnique(
 - new UserGestureIndicator(m_mouseCaptureGestureToken.release()));
 - break;
 - }
 - }
 
它里面會根據(jù)輸入事件的類型如mouseup、touchstart、keybord事件等類型去調(diào)不同的函數(shù)。click是在mouseup里面處理的,接著在MouseEventManager里面創(chuàng)建一個MouseEvent,并調(diào)度事件,即捕獲和冒泡:
- WebInputEventResult MouseEventManager::dispatchMouseEvent(EventTarget* target, const AtomicString& mouseEventType, const PlatformMouseEvent& mouseEvent, EventTarget* relatedTarget, bool checkForListener) {
 - MouseEvent* event = MouseEvent::create( mouseEventType, targetNode->document().domWindow(), mouseEvent/*...*/);
 - DispatchEventResult dispatchResult = target->dispatchEvent(event);
 - return EventHandlingUtil::toWebInputEventResult(dispatchResult);
 - }
 
上面代碼第2行創(chuàng)建MouseEvent,第3行dispatch。我們來看一下這個事件是如何層層封裝成一個MouseEvent的:
上圖展示了從原始的msg轉(zhuǎn)化成了W3C標準的MouseEvent的過程。Blink的消息處理引擎把msg轉(zhuǎn)化成了WebInputEvent,這個event能夠直接靜態(tài)轉(zhuǎn)化成可讀的WebMouseEvent,也就是事件在底層的時候已經(jīng)被封裝成帶有相關數(shù)據(jù)且可讀的事件了,上層再把它這些數(shù)據(jù)轉(zhuǎn)化成W3C規(guī)定格式的MouseEvent。
我們重點看下MouseEvent的create函數(shù):
- MouseEvent* MouseEvent::create(const AtomicString& eventType, AbstractView* view, const PlatformMouseEvent& event, Node* relatedTarget) {
 - bool isMouseEnterOrLeave = eventType == EventTypeNames::mouseenter ||
 - eventType == EventTypeNames::mouseleave;
 - bool isCancelable = !isMouseEnterOrLeave;
 - bool isBubbling = !isMouseEnterOrLeave;
 - return MouseEvent::create(
 - eventType, isBubbling, isCancelable, view, event.position().x()
 - /*.../*, &event);
 - }
 
從代碼第五行可以看到鼠標事件的mouseenter和mouseleave是不會冒泡的。
另外,每個Event都有一個EventPath,記錄它冒泡的路徑:
在dispatchEvent的時候,會初始化EventPath:
- void EventPath::initialize() {
 - if (eventPathShouldBeEmptyFor(*m_node, m_event))
 - return;
 - calculatePath();
 - calculateAdjustedTargets();
 - calculateTreeOrderAndSetNearestAncestorClosedTree();
 - }
 
第五行會去計算Path,而這個計算Path的核心邏輯非常簡單:
- void EventPath::calculatePath() {
 - // For performance and memory usage reasons we want to store the
 - // path using as few bytes as possible and with as few allocations
 - // as possible which is why we gather the data on the stack before
 - // storing it in a perfectly sized m_nodeEventContexts Vector.
 - HeapVector, 64> nodesInPath;
 - Node* current = m_node;
 - nodesInPath.push_back(current);
 - while (current) {
 - current = current->parentNode();
 - if (current)
 - nodesInPath.push_back(current);
 - }
 - m_nodeEventContexts.reserveCapacity(nodesInPath.size());
 - for (Node* nodeInPath : nodesInPath) {
 - m_nodeEventContexts.push_back(NodeEventContext(
 - nodeInPath, eventTargetRespectingTargetRules(*nodeInPath)));
 - }
 - }
 
第9行的while循環(huán)不斷地獲取當前node的父節(jié)點并把它push到一個vector里面,直到null即沒有父節(jié)點為止。***再把這個vector push到真正用來存儲成員變量。這段代碼我們又發(fā)現(xiàn)一個有趣的注釋,它說明了為什么不直接push到成員變量里面——因為vector變量會自動擴展本身大小,當push的時候容量不足時,會不斷地開辟內(nèi)存,blink的實現(xiàn)是開辟一個單位元素的空間,剛好存放一個元素:
- ptr = expandCapacity(size() + 1, ptr);
 
所以如果直接push_back到成員變量,會不斷地開辟新內(nèi)存。于是它一開始就初始化了一個size為64的棧變量來存放,減少開辟內(nèi)存的操作。另外有些vector自動擴充容量的實現(xiàn),可能是size * 1.5或者size + 10,而不是size + 1,這種情況就會導致有多余的空間沒用到。
通過這樣的手段,就有了記錄事件冒泡路徑的EventPath。
6. 事件捕獲和冒泡的實現(xiàn)
上面第5點提到的MouseEventManager會調(diào)dispatchEvent,這個函數(shù)會先創(chuàng)建一個dispatcher,這個dispatcher實例化的時候就會去初始化上面的EventPath,然后再進行dispatch/事件調(diào)度:
- EventDispatcher dispatcher(node, &mediator->event());
 - DispatchEventResult dispatchResult = dispatcher.dispatch();
 
所以核心函數(shù)就是第2行調(diào)的dispatch,而這個函數(shù)最核心的3行代碼為:
- if (dispatchEventAtCapturing() == ContinueDispatching) {
 - if (dispatchEventAtTarget() == ContinueDispatching)
 - dispatchEventAtBubbling();
 - }
 
(1)先執(zhí)行Capturing,然后再執(zhí)行AtTarget,***再Bubbling,我們來看一下Capturing函數(shù):
- inline EventDispatchContinuation EventDispatcher::dispatchEventAtCapturing() {
 - // Trigger capturing event handlers, starting at the top and working our way
 - // down.
 - //改變event的階段為冒泡
 - m_event->setEventPhase(Event::kCapturingPhase);
 - //先處理綁在window上的事件,并且如果event的m_propagationStopped被設置為true
 - //則返回done狀態(tài),不再繼續(xù)傳播
 - if (m_event->eventPath().windowEventContext().handleLocalEvents(*m_event) &&
 - m_event->propagationStopped())
 - return DoneDispatching;
 
上面做了一些初始化的工作后,循環(huán)EventPath依次觸發(fā)響應函數(shù):
- //從EventPath***一個元素,即最頂層的父結(jié)點開始下濾
 - for (size_t i = m_event->eventPath().size() - 1; i > 0; --i) {
 - const NodeEventContext& eventContext = m_event->eventPath()[i];
 - //觸發(fā)事件響應函數(shù)
 - eventContext.handleLocalEvents(*m_event);
 - //如果響應函數(shù)設置了stopPropagation,則返回done
 - if (m_event->propagationStopped())
 - return DoneDispatching;
 - }
 - return ContinueDispatching;
 - }
 
注意上面的for循環(huán)終止條件的i是大于0,i為0則為currentTarget。而總的size為6,與我們上面demo控制臺打印一致。
(2)at-target的處理就很簡單了,取i為0的那個Node并觸發(fā)它的listeners:
- inline EventDispatchContinuation EventDispatcher::dispatchEventAtTarget() {
 - m_event->setEventPhase(Event::kAtTarget);
 - m_event->eventPath()[0].handleLocalEvents(*m_event);
 - return m_event->propagationStopped() ? DoneDispatching : ContinueDispatching;
 - }
 
(3)bubbling的處理稍復雜,因為它還要處理cancleBubble的情況,不過總體的邏輯是類似的,核心代碼如下:
- inline void EventDispatcher::dispatchEventAtBubbling() {
 - // Trigger bubbling event handlers, starting at the bottom and working our way
 - // up.
 - size_t size = m_event->eventPath().size();
 - for (size_t i = 1; i < size; ++i) {
 - const NodeEventContext& eventContext = m_event->eventPath()[i];
 - if (m_event->bubbles() && !m_event->cancelBubble()) {
 - m_event->setEventPhase(Event::kBubblingPhase);
 - }
 - eventContext.handleLocalEvents(*m_event);
 - if (m_event->propagationStopped())
 - return;
 - }
 - }
 
可以看到bubbling的for循環(huán)是從i = 1開始,和capturing相反。因為bubble是三個階段***處理的,所以它不用再返回一個標志了。
上面介紹完了事件的捕獲和冒泡,我們注意到一個細節(jié),所有的事件都會先在capture階段在windows上觸發(fā)。
綜合以上,本文從源碼角度介紹了事件的數(shù)據(jù)結(jié)構(gòu),從一個側(cè)面解綁事件介紹事件和DOM節(jié)點的聯(lián)系,然后重點分析了事件的捕獲及冒泡過程。相信看完本文,對事件的本質(zhì)會有一個更透徹的理解。



















 
 
 





 
 
 
 