本文以 React v16.5.2 爲基礎進行源碼分析node
基本流程react
在 react源碼的 react-dom/src/events/ReactBrowserEventEmitter.js文件的開頭,有這麼一大段註釋:數組
事件委託是很經常使用的一種瀏覽器事件優化策略,因而 React就接管了這件事情,而且還貼心地消除了瀏覽器間的差別,賦予開發者跨瀏覽器的開發體驗,主要是使用 EventPluginHub這個東西來負責調度事件的存儲,合成事件並以對象池的方式實現建立和銷燬,至於下面的結構圖形,則是對事件機制的一個圖形化描述瀏覽器
#開始bash
<button onClick={this.autoFocus}>點擊聚焦</button>
複製代碼
這是咱們在React中綁定事件的常規寫法。經由JSX解析,button會被當作組件掛載。而onClick這時候也只是一個普通的props。 ReactDOMComponent在進行組件加載(mountComponent)、更新(updateComponent)的時候,須要對props進行處理(_updateDOMProperties):app
ReactDOMComponent.Mixin = {
mountComponent:function(){},
_createOpenTagMarkupAndPutListeners:function(){},
....,
// 方法中有指向上次屬性值得lastProp,
// nextProp是當前屬性值,這裏nextProp是咱們綁定給組件的onclick事件處理函數。
// nextProp 不爲空調用enqueuePutListener綁定事件,爲空則註銷事件綁定。
_updateDOMProperties:function(lastProps, nextProps, transaction){
for (propKey in lastProps) {} //省略。。。
for (propKey in nextProps) {
// 判斷是否爲事件屬性
if (registrationNameModules.hasOwnProperty(propKey)) {
enqueuePutListener(this, propKey, nextProp, transaction);
}
}
}
}
//這裏進行事件綁定
首先判斷了 rootContainerElement是否是一個 document或者 Fragment(文檔片斷節點)
enqueuePutListener 這個方法只在瀏覽器環境下執行,傳給listenTo參數分別是事件名稱'onclick'和代理事件的綁
定dom。若是是fragement 就是根節點(在reactDom.render指定的),不是的話就是document。listenTo
用於綁定事件到 document ,下面交由事務處理的是回調函數的存儲,便於調用。
ReactBrowserEventEmitter 文件中的 listenTo 看作事件處理的源頭。
這裏獲取了當前組件(其實這時候就是button)所在的document
function enqueuePutListener(inst, registrationName, listener, transaction) {
...
var containerInfo = inst._hostContainerInfo;
var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
listenTo(registrationName, doc);
...
}
複製代碼
綁定的重點是這裏的listenTo方法。看源碼(ReactBrowerEventEmitter)dom
//registrationName:須要綁定的事件
//當前component所屬的document,即事件須要綁定的位置
listenTo: function (registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
//獲取當前document上已經綁定的事件
var isListening = getListeningForDocument(mountAt);
// 獲取 registrationName(註冊事件名稱)的topLevelEvent(頂級事件類型)
var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
if (dependency === 'topWheel') {
...
} else if (dependency === 'topScroll') {
...
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
...
} else if (topEventMapping.hasOwnProperty(dependency)) {
// 獲取 topLevelEvent 對應的瀏覽器原生事件
//冒泡處理
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
isListening[dependency] = true;
}
}
},
複製代碼
對於同一個事件,例如click有兩個事件 onClick(在冒泡階段觸發) onClickCapture(在捕獲階段觸發)兩個事件名,這個冒泡和捕獲都是react事件模擬出來的。綁定到 document上面的事件基本上都是在冒泡階段(對 whell, focus, scroll 有額外處理),以下圖 click 事件綁定執行的以下。函數
最後處理(EventListener的listen和capture中)源碼分析
//eventType:事件類型,target: document對象,
//callback:是固定的,始終是ReactEventListener的dispatch方法
if (target.addEventListener) {
target.addEventListener(eventType, callback, false);
return {
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
};
}
複製代碼
全部事件綁定在document上性能
因此事件觸發的都是ReactEventListener的dispatch方法
看到這邊你可能疑惑,全部回調都執行的ReactEventListener的dispatch方法,那我寫的回調幹嗎去了。別急,接着看:
function enqueuePutListener(inst, registrationName, listener, transaction) {
...
//注意這裏!!!!!!!!!
//這裏獲取了當前組件(其實這時候就是button)所在的document
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
//事件綁定
listenTo(registrationName, doc);
//這段代碼表示將putListener放入回調序列,當組件掛載完成是會依次執行序列中的回調。putListener也是在那時候執行的。
//不明白的能夠看看本專欄中前兩篇關於transaction和掛載機制的講解
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
//保存回調
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}
}
複製代碼
仍是這段代碼,事件綁定咱們介紹過,主要是listenTo方法。 當綁定完成之後會執行putListener。該方法會在ReactReconcileTransaction事務的close階段執行,具體由EventPluginHub來進行管理
//
var listenerBank = {};
var getDictionaryKey = function (inst) {
//inst爲組建的實例化對象
//_rootNodeID爲組件的惟一標識
return '.' + inst._rootNodeID;
}
var EventPluginHub = {
//inst爲組建的實例化對象
//registrationName爲事件名稱
//listner爲咱們寫的回調函數,也就是列子中的this.autoFocus
putListener: function (inst, registrationName, listener) {
...
var key = getDictionaryKey(inst);
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
bankForRegistrationName[key] = listener;
...
}
}
複製代碼
EventPluginHub在每一個項目中只實例化一次。也就是說,項目組全部事件的回調都會儲存在惟一的listenerBank中。
是否是有點暈,放上流程圖,仔細回憶一下
註冊事件時咱們說過,全部的事件都是綁定在Document上。回調統一是ReactEventListener的dispatch方法。 因爲冒泡機制,不管咱們點擊哪一個DOM,最後都是由document響應(由於其餘DOM根本沒有事件監聽)。也便是說都會觸發dispatch
dispatchEvent: function(topLevelType, nativeEvent) {
//實際觸發事件的DOM對象
var nativeEventTarget = getEventTarget(nativeEvent);
//nativeEventTarget對應的virtual DOM
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
nativeEventTarget,
);
...
//建立bookKeeping實例,爲handleTopLevelImpl回調函數傳遞事件名和原生事件對象
//其實就是把三個參數封裝成一個對象
var bookKeeping = getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
);
try {
//這裏開啓一個transactIon,perform中執行了
//handleTopLevelImpl(bookKeeping)
ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
},
複製代碼
function handleTopLevelImpl(bookKeeping) {
//觸發事件的真實DOM
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
//nativeEventTarget對應的ReactElement
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
//bookKeeping.ancestors保存的是組件。
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];
//具體處理邏輯
ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
}
}
複製代碼
//這就是核心的處理了
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
//首先封裝event事件
var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
//發送包裝好的event
runEventQueueInBatch(events);
}
複製代碼
首先是EventPluginHub的extractEvents
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var events;
var plugins = EventPluginRegistry.plugins;
for (var i = 0; i < plugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
var possiblePlugin = plugins[i];
if (possiblePlugin) {
//主要看這邊
var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
......
}
}
return events;
},
複製代碼
接着看SimpleEventPlugin的方法
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
......
//這裏是對事件的封裝,可是不是咱們關注的重點
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
//重點看這邊
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;
}
複製代碼
接下來是方法中的各類引用,跳啊跳,轉啊轉,咱們來到了ReactDOMTraversal中的traverseTwoPhase方法
//inst是觸發事件的target的ReactElement
//fn:EventPropagator的accumulateDirectionalDispatches
//arg: 就是以前部分封裝好的event(之因此說是部分,是由於如今也是在處理Event,這邊處理完纔是封裝完成)
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
//注意path,這裏以ReactElement的形式冒泡着,
//把觸發事件的父節點依次保存下來
path.push(inst);
//獲取父節點
inst = inst._hostParent;
}
var i;
//捕捉,依次處理
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
//冒泡,依次處理
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
複製代碼
//判斷父組件是否保存了這一類事件
function accumulateDirectionalDispatches(inst, phase, event) {
//獲取到回調
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
//若是有回調,就把包含該類型事件監聽的DOM與對應的回調保存進Event。
//accumulateInto能夠理解成_.assign
//記住這兩個屬性,很重要。
event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
複製代碼
listenerAtPhase裏面執行的是EventPluginHub的getListener函數
getListener: function (inst, registrationName) {
//還記得以前保存回調的listenerBank吧?
var bankForRegistrationName = listenerBank[registrationName];
if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
return null;
}
//獲取inst的_rootNodeId
var key = getDictionaryKey(inst);
//獲取對應的回調
return bankForRegistrationName && bankForRegistrationName[key];
},
複製代碼
runEventQueueInBatch主要進行了兩步操做
function runEventQueueInBatch(events) {
//將event事件加入processEventQueue序列
EventPluginHub.enqueueEvents(events);
//前一步保存好的processEventQueue依次執行
//executeDispatchesAndRelease
EventPluginHub.processEventQueue(false);
}
processEventQueue: function (simulated) {
var processingEventQueue = eventQueue;
eventQueue = null;
if (simulated) {
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
} else {
//重點看這裏
//forEachAccumulated能夠當作forEach的封裝
//那麼這裏就是processingEventQueue保存的event依次執行executeDispatchesAndReleaseTopLevel(event)
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
}
},
複製代碼
executeDispatchesAndReleaseTopLevel(event)又是各類函數包裝,最後幹活的是
function executeDispatchesInOrder(event, simulated) {
//對應的回調函數數組
var dispatchListeners = event._dispatchListeners;
//有eventType屬性的ReactElement數組
var dispatchInstances = event._dispatchInstances;
......
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) {
break;
}
executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
}
} else if (dispatchListeners) {
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
}
event._dispatchListeners = null;
event._dispatchInstances = null;
}
複製代碼
OK,這裏總算出現了老熟人,在封裝nativeEvent時咱們保存在event裏的兩個屬性,dispatchListeners與dispatchInstances,在這裏起做用。 代碼很簡單,若是有處理這個事件的回調函數,就一次進行處理。細節咱們稍後討論,先看看這裏是怎麼處理的吧
function executeDispatch(event, simulated, listener, inst) {
//type是事件類型
var type = event.type || 'unknown-event';
//這是觸發事件的真實DOM,也就是列子中的button
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
//看這裏看這裏
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
複製代碼
終於來到最後了,代碼位於ReactErrorUtil中 (爲了幫助開發,React經過模擬真正的瀏覽器事件來得到更好的devtools集成。這段代碼在開發模式下運行)
//創造一個臨時DOM
var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
//綁定回調函數的上下文
var boundFunc = func.bind(null, 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);
};
複製代碼