一看就暈的React事件機制

一看就暈的React事件機制

原文作者:IMWeb 黃qiong

原文鏈接:imweb.io/topic/5aa0de1f

前言

本篇文章我們從源碼來深挖一下React的事件機制。

TL;DR :

  • react事件機制分為兩個部分:1、事件註冊 2、事件分發
  • 事件註冊部分,所有的事件都會註冊到document上,擁有統一的回調函數dispatchEvent來執行事件分發
  • 事件分發部分,首先生成合成事件,注意同一種事件類型只能生成一個合成事件Event,如onclick這個類型的事件,dom上所有帶有通過jsx綁定的onClick的回調函數都會按順序(冒泡或者捕獲)會放到Event._dispatchListeners 這個數組裡,後面依次執行它。

還是使用上次的栗子:

分析源碼之前,有些工作和知識要提前準備,普及一下:

  • 請各位準備好一個編輯器,自行用react-starter-kit建一個react項目,複製上面的代碼,渲染上面的組件,然後打開控制台
  • 下圖是整個事件機制的流程圖,後面會分部分解析 processon.com/diagramin
  • 普及幾個功能函數,提前了解它的作用

React事件機制

React事件機制分為兩塊:

  • 事件註冊
  • 事件分發

我們一步步來看:

事件註冊

整個過程從ReactDomComponent開始,重點在enqueuePutListener,這個函數做了三件事情,詳細請參考下面源碼:

ReactDomComponent.js

接下來看看第二步:在document上註冊事件 的過程,流程圖如下:

接著我們抽出每個文件的重點函數出來分析:

ReactBrowserEventEmitter.js

由於onclick綁定的是冒泡事件,所以我們來看看trapBubbledEvent

ReactEventListener.js(圖片可左右滑動)

// 輸入: topClick, click, doctrapBubbledEvent: function (topLevelType, handlerBaseName, element) { if (!element) { return null; } // EventListener 要做的事情就是把事件綁定到document上,注意這裡無論是註冊冒泡還是捕獲事件,最終的回調函數都是dispatchEvent return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType)); },// EventListener.js// 輸入doc, click, dispatchEvent// 這個函數其實就是我們熟悉的兼容瀏IE瀏覽器事件綁定的方法listen: function listen(target, eventType, callback) { if (target.addEventListener) { target.addEventListener(eventType, callback, false); return { remove: function remove() { target.removeEventListener(eventType, callback, false); } }; } else if (target.attachEvent) { target.attachEvent(on + eventType, callback); return { remove: function remove() { target.detachEvent(on + eventType, callback); } }; } },

注意這裡無論是註冊冒泡還是捕獲事件,最終的回調函數都是dispatchEvent,所以我們來看看dispatchEvent怎麼處理事件分發。

dispatchEvent

看到這裡大家會奇怪,所有的事件的回調函數都是dispatchEvent來處理,那事件onClick原來的回調函數存到哪裡去了呢?

再回來看事件註冊的第三步:mountReady之後將回調函數存在ListernBank中

ReactDomComponent.js

function enqueuePutListener () { // 省略部分代碼 ... // 1、*重要:在這裡取出button所在的document* var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; // 2、在document上註冊事件,同一個事件類型只會被註冊一次 listenTo(registrationName, doc); // 3、mountReady之後將回調函數存在ListernBank中 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener });}

document上註冊完所有的事件之後,還需要把listener 放到listenerBank中以listenerBank[registrationName][key]這樣的形式存起來,然後在dispatchEvent裡面使用。

將listener放到listenerBank中儲存的過程如下:

ReactDomComponent.js

// 在putListener里存入listenerfunction putListener() { var listenerToPut = this; // 先put的是外層的listener - outClick,所以這裡的inst是外層div // registrationName是onclick,listener是outClick EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);}

EventPluginHub.js

以上就是事件註冊的過程,接下來在看dispatchEvent如何處理事件分發。

事件分發

在介紹事件分發之前,有必要先介紹一下生成合成事件的過程,鏈接是segmentfault.com/a/1190

了解合成事件生成的過程之後,我們需要get一個點:合成事件收集了一波同類型(例如click)的回調函數存在了合成事件event._dispatchListeners這個數組裡,然後將它們事件對應的虛擬dom節點放到_dispatchInstances 就本例來說,_dispatchListeners= [onClick, outClick],之後在一起執行。

接下來看看事件分發的過程:

EventListener.js

dispatchEvent: function (topLevelType, nativeEvent) { if (!ReactEventListener._enabled) { return; } // 這裡得到TopLevelCallbackBookKeeping的實例對象,本例中第一次觸發dispatchEvent時 // bookKeeping = {ancestors: [],nativeEvent,『topClick』} var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent); try { // Event queue being processed in the same cycle allows // `preventDefault`. // 接著執行handleTopLevelImpl(bookKeeping) ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } }function handleTopLevelImpl(bookKeeping) { var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent); // 獲取當前事件的虛擬dom元素 var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget); var ancestor = targetInst; do { bookKeeping.ancestors.push(ancestor); ancestor = ancestor && findParent(ancestor); } while (ancestor); for (var i = 0; i < bookKeeping.ancestors.length; i++) { targetInst = bookKeeping.ancestors[i]; // 這裡的_handleTopLevel 對應的就是ReactEventEmitterMixin.js里的handleTopLevel ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); }}// 這裡的findParent曾經給我帶來誤導,我以為去找當前元素所有的父節點,但其實不是的,// 我們知道一般情況下,我們的組件最後會被包裹在<div id=root></div>的標籤里// 一般是沒有組件再去嵌套它的,所以通常返回null/** * Find the deepest React component completely containing the root of the * passed-in instance (for use when entire React trees are nested within each * other). If React trees are not nested, returns null. */function findParent(inst) { while (inst._hostParent) { inst = inst._hostParent; } var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst); var container = rootNode; return ReactDOMComponentTree.getClosestInstanceFromNode(container);}

上面這段代碼的重點就是_handleTopLevel,它可以獲取合成事件,並且去執行它。

下面看看具體是如何執行:

ReactEventEmitterMixin.js

function runEventQueueInBatch(events) { // 1、先將事件放進隊列里 EventPluginHub.enqueueEvents(events); // 2、執行它 EventPluginHub.processEventQueue(false);}var ReactEventEmitterMixin = { /** * Streams a fired top-level event to `EventPluginHub` where plugins have the * opportunity to create `ReactEvent`s to be dispatched. */ handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { // 用EventPluginHub生成合成事件 var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); // 執行合成事件 runEventQueueInBatch(events); }};

執行的過程分成兩步:

  • 將事件放進隊列
  • 執行

執行的細節如下:

EventPluginHub.js

var executeDispatchesAndReleaseTopLevel = function (e) { return executeDispatchesAndRelease(e, false); }; var executeDispatchesAndRelease = function (event, simulated) { if (event) { // 在這裡dispatch事件 EventPluginUtils.executeDispatchesInOrder(event, simulated); // 釋放事件 if (!event.isPersistent()) { event.constructor.release(event); } } }; enqueueEvents: function (events) { if (events) { eventQueue = accumulateInto(eventQueue, events); } }, /** * Dispatches all synthetic events on the event queue. * * @internal */ processEventQueue: function (simulated) { // Set `eventQueue` to null before processing it so that we can tell if more // events get enqueued while processing. var processingEventQueue = eventQueue; eventQueue = null; if (simulated) { forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated); } else { forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel); } // This would be a good time to rethrow if any of the event fexers threw. ReactErrorUtils.rethrowCaughtError(); },

上段代碼里,我們最終會走到

forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);

forEachAccumulated這個函數我們之前講過,就是對數組processingEventQueue的每一個合成事件都使用executeDispatchesAndReleaseTopLevel來dispatch 事件。

所以各位同學們,注意到這裡我們已經走到最核心的部分,dispatch 合成事件了,下面看看dispatch的詳細過程:

EventPluginUtils.js

/** * Standard/simple iteration through an events collected dispatches. */function executeDispatchesInOrder(event, simulated) { var dispatchListeners = event._dispatchListeners; var dispatchInstances = event._dispatchInstances; if (Array.isArray(dispatchListeners)) { for (var i = 0; i < dispatchListeners.length; i++) { // 由這裡可以看出,合成事件的stopPropagation只能阻止react合成事件的冒泡, // 因為event._dispatchListeners 只記錄了由jsx綁定的綁定的事件,對於原生綁定的是沒有記錄的 if (event.isPropagationStopped()) { break; } // Listeners and Instances are two parallel arrays that are always in sync. executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]); } } else if (dispatchListeners) { executeDispatch(event, simulated, dispatchListeners, dispatchInstances); } event._dispatchListeners = null; event._dispatchInstances = null;}

由上面的函數可知,dispatch 合成事件分為兩個步驟:

  • 通過_dispatchListeners里得到所有綁定的回調函數,在通過_dispatchInstances的綁定回調函數的虛擬dom元素
  • 循環執行_dispatchListeners里所有的回調函數,這裡有一個特殊情況,也是react阻止冒泡的原理

當回調函數里使用了stopPropagation會使得數組後面的回調函數不能執行,這樣就做到了阻止事件冒泡。

目前還是還有看到執行事件的代碼,再接著看:

EventPluginHub.js

function executeDispatch(event, simulated, listener, inst) { var type = event.type || unknown-event; // 注意這裡將事件對應的dom元素綁定到了currentTarget上 event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) { ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); } else { // 一般都是非模擬的情況,執行invokeGuardedCallback ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null;}

上面這個函數最重要的功能就是將事件對應的dom元素綁定到了currentTarget上, 這樣我們通過e.currentTarget就可以找到綁定事件的原生dom元素。

下面就是整個執行過程的尾聲了:

ReactErrorUtils.js

var fakeNode = document.createElement(react);ReactErrorUtils.invokeGuardedCallback = function (name, func, a) { var boundFunc = function () { func(a); }; var evtType = react- + name; fakeNode.addEventListener(evtType, boundFunc, false); var evt = document.createEvent(Event); evt.initEvent(evtType, false, false); fakeNode.dispatchEvent(evt); fakeNode.removeEventListener(evtType, boundFunc, false);};

invokeGuardedCallback可知,最後react調用了faked元素的dispatchEvent方法來觸發事件,並且觸發完畢之後立即移除監聽事件。

總的來說,整個click事件被分發的過程就是:

1、用EventPluginHub生成合成事件,這裡注意同一事件類型只會生成一個合成事件,裡面的_dispatchListeners里儲存了同一事件類型的所有回調函數

2、按順序去執行它

就辣么簡單!

前端NEXT學位課程限時優惠中!感興趣的點擊下方鏈接找我們的助教小姐姐哦~

qm.qq.com/cgi-bin/qm/qr? (二維碼自動識別)


Hello~各位小夥伴!告訴你們一件超振奮的事情!

「NEXT儲備精英計劃」開啟熱血招募啦~~~

如果你是在校大學生,並在社團/協會/組織擔任要職,

或者你是擁有豐富校園社群資源的社交小達人。

歡迎來加入我們一起搞事情!

你將得到:

騰訊系活動項目實習證明+騰訊官方渠道「金榜題名」+內部直推面試機會!

心動嗎?那就行動吧!請點擊下方鏈接入群了解活動詳情!

qm.qq.com/cgi-bin/qm/qr? (二維碼自動識別)

推薦閱讀:

v-on 綁定事件時,函數名加括弧和不加括弧有什麼區別?
前端常見的框架總結
React源碼(一) setState
【備戰秋招Day 1】經典面試題1-4及在線編程題1-3答案
編程中的 Model 到底是什麼?

TAG:React | 前端開發 | 前端框架 |