從Chrome源碼看瀏覽器的事件機(jī)制
在上一篇《從Chrome源碼看瀏覽器如何構(gòu)建DOM樹》介紹了blink如何創(chuàng)建一棵DOM樹,在這一篇將介紹事件機(jī)制。
上一篇還有一個(gè)地方未提及,那就是在構(gòu)建完DOM之后,瀏覽器將會(huì)觸發(fā)DOMContentLoaded事件,這個(gè)事件是在處理tokens的時(shí)候遇到EndOfFile標(biāo)志符時(shí)觸發(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時(shí),將會(huì)在第6行停止解析。這是***一個(gè)待處理的token,一般是跟在后面的一個(gè)\EOF標(biāo)志符來的。
第6行的prepareToStopParsing,會(huì)在Document的finishedParseing里面生成一個(gè)事件,再調(diào)用dispatchEvent,進(jìn)一步調(diào)用監(jiān)聽函數(shù):
- void Document::finishedParsing() {
- dispatchEvent(Event::createBubble(EventTypeNames::DOMContentLoaded));
- }
這個(gè)dispatchEvent是EventTarget這個(gè)類的成員函數(shù)。在上一篇描述DOM的結(jié)點(diǎn)數(shù)據(jù)結(jié)構(gòu)時(shí)將Node作為根結(jié)點(diǎn),其實(shí)Node上面還有一個(gè)類,就是EventTarget。我們先來看一下事件的數(shù)據(jù)結(jié)構(gòu)是怎么樣的:
1. 事件的數(shù)據(jù)結(jié)構(gòu)
畫出事件相關(guān)的類圖:
在最頂層的EventTarget提供了三個(gè)函數(shù),分別是添加監(jiān)聽add、刪除監(jiān)聽remove、觸發(fā)監(jiān)聽fire。一個(gè)典型的訪問者模式我在《Effective前端5:減少前端代碼耦合》提到了,這里重點(diǎn)看一下blink實(shí)際上是怎么實(shí)現(xiàn)的。
在Node類組合了一個(gè)EventTargetDataMap,這是一個(gè)哈希map,并且它是靜態(tài)成員變量。它的key值是當(dāng)前結(jié)點(diǎn)Node實(shí)例的指針,value值是事件名稱和對(duì)應(yīng)的listeners。如果畫一個(gè)示例圖,它的存儲(chǔ)是這樣的:
如上,按照正常的思維,存放事件名稱和對(duì)應(yīng)的訪問者應(yīng)該是用一個(gè)哈希map,但是blink卻是用的向量vector + pair,這就導(dǎo)致在查找某個(gè)事件的訪問者的時(shí)候,需要循環(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é)省空間,并且一個(gè)dom節(jié)點(diǎn)往往不太可能綁了太多的事件類型。這就啟示我們寫代碼要根據(jù)實(shí)際情況靈活處理。
同時(shí)還有一個(gè)比較有趣的事情,就是webkit用了一個(gè)EventTargetDataMap存放所有節(jié)點(diǎn)綁定的事件,它是一個(gè)static靜態(tài)成員變量,被所有Node的實(shí)例所共享,由于不同的實(shí)例的內(nèi)存地址不一樣,所以它的key不一樣,就可以通過內(nèi)存地址找到它綁的所有事件,即上面說的vector結(jié)構(gòu)。為什么它要用一個(gè)類似于全局的變量?按照正常思維,每個(gè)Node結(jié)點(diǎn)綁的事件是獨(dú)立的,那應(yīng)該把綁的事件作為每個(gè)Node實(shí)例獨(dú)立的數(shù)據(jù),搞一個(gè)全局的還得用一個(gè)map作一個(gè)哈希映射。
一個(gè)可能的原因是EventTarget是作為所有DOM結(jié)點(diǎn)的事件目標(biāo)的類,除了Node之外,還有FileReader、AudioNode等也會(huì)繼承于EventTarget,它們有另外一個(gè)EventTargetData。把所有的事件都放一起了,應(yīng)該會(huì)方便統(tǒng)一處理。
這個(gè)時(shí)候你可能會(huì)冒出另外一個(gè)問題,這個(gè)EventTargetDataMap是什么釋放綁定的事件的,我把一個(gè)DOM結(jié)點(diǎn)刪了,它會(huì)自動(dòng)去釋放綁定的的事件嗎?換句話說,刪除掉一個(gè)結(jié)點(diǎn)前需不需要先off掉它的事件?
2. DOM結(jié)點(diǎn)刪除與事件解綁
從源碼可以看到,Node的析構(gòu)函數(shù)并沒有去釋放當(dāng)前Node綁定的事件,所以它是不是不會(huì)自動(dòng)釋放事件?為驗(yàn)證,我們?cè)谔砑咏壎ㄒ粋€(gè)事件后、刪掉結(jié)點(diǎn)后分別打印這個(gè)map里面的數(shù)據(jù),為此給Node添加一個(gè)打印的函數(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é)點(diǎn)的標(biāo)簽名。
同時(shí)試驗(yàn)的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é)點(diǎn)之后,它的事件依然存在。
我們看一下blink在remove里面做了什么:
- void Node::remove(ExceptionState& exceptionState) {
- if (ContainerNode* parent = parentNode())
- parent->removeChild(this, exceptionState);
- }
remove是后來W3C新加的api,所以在remove里面調(diào)的是老的removeChild,removeChild的關(guān)鍵代碼如下:
- 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);
前面幾行是重新設(shè)置DOM樹的結(jié)點(diǎn)關(guān)系,比較好理解。***面三行,把刪除掉的結(jié)點(diǎn)的兄弟指針和父指針置為null,注意這里并沒有把它delete掉,只是把它隔離開來。所以把它remove掉之后, 這個(gè)結(jié)點(diǎn)在內(nèi)存里面依舊存在,你依然可以獲取它的innerText,把它重新append到body里面(但是不推薦這么做)。同時(shí)事件依然存在那個(gè)map里面。
什么時(shí)候這個(gè)節(jié)點(diǎn)會(huì)被真正的析構(gòu)呢?發(fā)生在GC回收的時(shí)候,GC回收的時(shí)候會(huì)把DOM結(jié)點(diǎn)的內(nèi)存釋放,并且會(huì)刪掉map里面的數(shù)據(jù)。為驗(yàn)證,在啟動(dòng)Chrome的時(shí)候加上參數(shù):
- chromium test.html --js-flags='--expose_gc'
這樣可以調(diào)用window.gc觸發(fā)gc回收,然后在上面的js demo代碼后面加上:
- setTimeout(function(){
- //添加這個(gè)事件是為了觸發(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標(biāo)簽并更新存放事件的數(shù)據(jù)結(jié)構(gòu)。
所以說刪掉一個(gè)DOM結(jié)點(diǎn),并不需要手動(dòng)去釋放它的事件。
需要注意的是DOM結(jié)點(diǎn)一旦存在一個(gè)引用,即使你把它remove掉了,GC也不會(huì)去回收,如下:
- <script>
- var p = document.getElementById("text");
- p.remove();
- window.gc();
- </script>
執(zhí)行了window.gc之后并不會(huì)去回收p的內(nèi)存空間以及它的事件。因?yàn)檫€存在一個(gè)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>
自動(dòng)銷毀,p結(jié)點(diǎn)沒有人引用了,能夠自動(dòng)GC回收。
還有一個(gè)問題一直困擾著我,那就是監(jiān)聽X按鈕的click,然后把它的父容器如彈框給刪了,這樣它自已本身也刪了,但是監(jiān)聽函數(shù)還可以繼續(xù)執(zhí)行,實(shí)體都沒有了,為什么綁在它身上的函數(shù)還可以繼續(xù)執(zhí)行呢?通過上面的分析,應(yīng)該可以找到答案:刪掉之后GC并不會(huì)立刻回收和釋放事件,因?yàn)樵趫?zhí)行監(jiān)聽函數(shù)的時(shí)候,里面有個(gè)this指針指向了該節(jié)點(diǎn),并且this是只讀的,你不能把它置成null。所以只有執(zhí)行完了回調(diào)函數(shù),離開了作用域,this才會(huì)銷毀,才有可能被GC回收。
還有一種綁事件的方式,沒有討論:
3. DOM Level 0事件
就是使用dom結(jié)點(diǎn)的onclick、onfocus等屬性,添加事件,由于這個(gè)提得比較早,所以它的兼容性***。如下:
- function clickHandle(){
- console.log("addEventListener click");
- }
- var p = document.getElementById("text");
- p.addEventListener("click", clickHandle);
- p.onclick = function(){
- console.log("onclick trigger");
- };
如果點(diǎn)擊p標(biāo)簽,將會(huì)觸發(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ù)里面會(huì)去獲取上一次的listener,然后調(diào)removeEventListener,關(guān)鍵在于它怎么根據(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é)點(diǎn)該事件名稱的所有l(wèi)istener做個(gè)循環(huán),然后判斷這個(gè)listener是否為屬性事件。判斷成立,則返回。怎么判斷是否為屬性事件?那個(gè)是實(shí)例化事件的時(shí)候封裝好的了。
從上面的源代碼可以很清楚地看到onclick等屬性事件只能綁一次,并且和addEventListener的事件不沖突。
關(guān)于事件,還有一個(gè)很重要的概念,那就是事件的捕獲和冒泡。
4. 事件的捕獲和冒泡
用以下html做試驗(yàn):
- <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);
第三個(gè)參數(shù)為true,表示監(jiān)聽在捕獲階段,點(diǎn)擊p標(biāo)簽之后控制臺(tái)打印出:
- [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類定義里面可以找到關(guān)到eventPhase的定義:
- enum PhaseType {
- kNone = 0,
- kCapturingPhase = 1,
- kAtTarget = 2,
- kBubblingPhase = 3
- };
1表示捕獲取階段,2表示在當(dāng)前目標(biāo),3表示冒泡階段。把上面的phase轉(zhuǎn)化成文字,并把html/body/document也綁上事件,同時(shí)at-target只綁一次,那么整一個(gè)過程將是這樣的:
- “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一直捕獲到目標(biāo)div3,然后再一直冒泡到document,如果在某個(gè)階段執(zhí)行了:
- event.stopPropagation()
那么后續(xù)的過程將不會(huì)繼續(xù),例如在document的capture階段的click事件里面執(zhí)行了上面的阻止傳播函數(shù),那么控制臺(tái)只會(huì)打印出上面輸出的***行。
在研究blink是如何實(shí)現(xiàn)之前,我們先來看一下事件是怎么觸發(fā)和封裝的
5. 事件的觸發(fā)和封裝
以click事件為例,Blink在RenderViewImpl里面收到了外面的進(jìn)程的消息:
- // 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是頁面最基礎(chǔ)的一個(gè)類,當(dāng)它收到IPC發(fā)來的消息時(shí),根據(jù)消息的類型,調(diào)用相應(yīng)的處理函數(shù),由于這是一個(gè)input消息,所以它會(huì)調(diào):
- IPC_MESSAGE_HANDLER(InputMsg_HandleInputEvent, OnHandleInputEvent)
上面的IPC_MESSAGE_HANDLER其實(shí)是Blink定義的一個(gè)宏,這個(gè)宏其實(shí)就是一個(gè)switch-case里面的case。
這個(gè)處理函數(shù)又會(huì)調(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;
- }
- }
它里面會(huì)根據(jù)輸入事件的類型如mouseup、touchstart、keybord事件等類型去調(diào)不同的函數(shù)。click是在mouseup里面處理的,接著在MouseEventManager里面創(chuàng)建一個(gè)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。我們來看一下這個(gè)事件是如何層層封裝成一個(gè)MouseEvent的:
上圖展示了從原始的msg轉(zhuǎn)化成了W3C標(biāo)準(zhǔn)的MouseEvent的過程。Blink的消息處理引擎把msg轉(zhuǎn)化成了WebInputEvent,這個(gè)event能夠直接靜態(tài)轉(zhuǎn)化成可讀的WebMouseEvent,也就是事件在底層的時(shí)候已經(jīng)被封裝成帶有相關(guān)數(shù)據(jù)且可讀的事件了,上層再把它這些數(shù)據(jù)轉(zhuǎn)化成W3C規(guī)定格式的MouseEvent。
我們重點(diǎn)看下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);
- }
從代碼第五行可以看到鼠標(biāo)事件的mouseenter和mouseleave是不會(huì)冒泡的。
另外,每個(gè)Event都有一個(gè)EventPath,記錄它冒泡的路徑:
在dispatchEvent的時(shí)候,會(huì)初始化EventPath:
- void EventPath::initialize() {
- if (eventPathShouldBeEmptyFor(*m_node, m_event))
- return;
- calculatePath();
- calculateAdjustedTargets();
- calculateTreeOrderAndSetNearestAncestorClosedTree();
- }
第五行會(huì)去計(jì)算Path,而這個(gè)計(jì)算Path的核心邏輯非常簡(jiǎn)單:
- 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)不斷地獲取當(dāng)前node的父節(jié)點(diǎn)并把它push到一個(gè)vector里面,直到null即沒有父節(jié)點(diǎn)為止。***再把這個(gè)vector push到真正用來存儲(chǔ)成員變量。這段代碼我們又發(fā)現(xiàn)一個(gè)有趣的注釋,它說明了為什么不直接push到成員變量里面——因?yàn)関ector變量會(huì)自動(dòng)擴(kuò)展本身大小,當(dāng)push的時(shí)候容量不足時(shí),會(huì)不斷地開辟內(nèi)存,blink的實(shí)現(xiàn)是開辟一個(gè)單位元素的空間,剛好存放一個(gè)元素:
- ptr = expandCapacity(size() + 1, ptr);
所以如果直接push_back到成員變量,會(huì)不斷地開辟新內(nèi)存。于是它一開始就初始化了一個(gè)size為64的棧變量來存放,減少開辟內(nèi)存的操作。另外有些vector自動(dòng)擴(kuò)充容量的實(shí)現(xiàn),可能是size * 1.5或者size + 10,而不是size + 1,這種情況就會(huì)導(dǎo)致有多余的空間沒用到。
通過這樣的手段,就有了記錄事件冒泡路徑的EventPath。
6. 事件捕獲和冒泡的實(shí)現(xiàn)
上面第5點(diǎn)提到的MouseEventManager會(huì)調(diào)dispatchEvent,這個(gè)函數(shù)會(huì)先創(chuàng)建一個(gè)dispatcher,這個(gè)dispatcher實(shí)例化的時(shí)候就會(huì)去初始化上面的EventPath,然后再進(jìn)行dispatch/事件調(diào)度:
- EventDispatcher dispatcher(node, &mediator->event());
- DispatchEventResult dispatchResult = dispatcher.dispatch();
所以核心函數(shù)就是第2行調(diào)的dispatch,而這個(gè)函數(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被設(shè)置為true
- //則返回done狀態(tài),不再繼續(xù)傳播
- if (m_event->eventPath().windowEventContext().handleLocalEvents(*m_event) &&
- m_event->propagationStopped())
- return DoneDispatching;
上面做了一些初始化的工作后,循環(huán)EventPath依次觸發(fā)響應(yīng)函數(shù):
- //從EventPath***一個(gè)元素,即最頂層的父結(jié)點(diǎn)開始下濾
- for (size_t i = m_event->eventPath().size() - 1; i > 0; --i) {
- const NodeEventContext& eventContext = m_event->eventPath()[i];
- //觸發(fā)事件響應(yīng)函數(shù)
- eventContext.handleLocalEvents(*m_event);
- //如果響應(yīng)函數(shù)設(shè)置了stopPropagation,則返回done
- if (m_event->propagationStopped())
- return DoneDispatching;
- }
- return ContinueDispatching;
- }
注意上面的for循環(huán)終止條件的i是大于0,i為0則為currentTarget。而總的size為6,與我們上面demo控制臺(tái)打印一致。
(2)at-target的處理就很簡(jiǎn)單了,取i為0的那個(gè)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的處理稍復(fù)雜,因?yàn)樗€要處理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相反。因?yàn)閎ubble是三個(gè)階段***處理的,所以它不用再返回一個(gè)標(biāo)志了。
上面介紹完了事件的捕獲和冒泡,我們注意到一個(gè)細(xì)節(jié),所有的事件都會(huì)先在capture階段在windows上觸發(fā)。
綜合以上,本文從源碼角度介紹了事件的數(shù)據(jù)結(jié)構(gòu),從一個(gè)側(cè)面解綁事件介紹事件和DOM節(jié)點(diǎn)的聯(lián)系,然后重點(diǎn)分析了事件的捕獲及冒泡過程。相信看完本文,對(duì)事件的本質(zhì)會(huì)有一個(gè)更透徹的理解。