原始碼看React 事件機制
對React熟悉的同學都知道,React中的事件機制並不是原生的那一套,事件沒有繫結在原生DOM上,發出的事件也是對原生事件的包裝。
那麼這一切是怎麼實現的呢?
事件註冊
首先還是看我們熟悉的程式碼
<button onClick={this.autoFocus}>點選聚焦</button>
這是我們在React中繫結事件的常規寫法。經由JSX解析,button會被當做元件掛載。而onClick
這時候也只是一個普通的props。
ReactDOMComponent在進行元件載入(mountComponent)、更新(updateComponent)的時候,需要對props進行處理(_updateDOMProperties):
ReactDOMComponent.Mixin = { _updateDOMProperties: function (lastProps, nextProps, transaction) { ... for (propKey in nextProps) { // 判斷是否為事件屬性 if (registrationNameModules.hasOwnProperty(propKey)) { enqueuePutListener(this, propKey, nextProp, transaction); } } } } //這裡進行事件繫結 function enqueuePutListener(inst, registrationName, listener, transaction) { ... //注意這裡!!!!!!!!! //這裡獲取了當前元件(其實這時候就是button)所在的document var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; listenTo(registrationName, doc); transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); function putListener() { var listenerToPut = this; EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); } }
繫結的重點是這裡的listenTo方法。看原始碼(ReactBrowerEventEmitter)
//registrationName:需要繫結的事件 //當前component所屬的document,即事件需要繫結的位置 listenTo: function (registrationName, contentDocumentHandle) { var mountAt = contentDocumentHandle; //獲取當前document上已經繫結的事件 var isListening = getListeningForDocument(mountAt); ... if (...) { //冒泡處理 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...); } else if (...) { //捕捉處理 ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...); } ... },
最後處理(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];
},
可以發現,React在分裝原生nativeEvent時
- 將有eventType屬性的ReactElement放入 event._dispatchInstances
- 將對應的回撥依次放入event._dispatchListeners
事件分發
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);
};
總體流程
不難發現,我們經歷了從真實DOM到Virtual DOM的來回轉化。
常見問題的答案。
- e.stopPropagation不能阻止原生事件冒泡
event是封裝好的事件。他是在document的回撥裡進行封裝,並執行回撥的。而原生的監聽,在document接收到冒泡時早就執行完了。 - e.nativeEvent.stopPropagation,回撥無法執行。
很簡單,因為冒泡是從裡到外,執行了原生的阻止冒泡,document當如捕捉不到,document都沒捕捉到,React還玩個球啊,要知道,一切操作都放在docuemnt的回撥裡了。 -
怎麼避免兩者影響
``` 這個答案大家說了很多次,避免原生事件與React事件混用,或者通過target進行判斷。 ```
為什麼這麼設計
在網上看過一個列子說得很好,
一個Ul下面有1000個li標籤。想在想為每個li都繫結一個事件,怎麼操作?
總不可能一個個繫結吧?
其實這個和jquery繫結事件差不多。通過最外層繫結事件,當操作是點選任何一個li自然會冒泡到最外面的Ul,又可以通過最外面的target獲取到具體操作的DOM。一次繫結,收益一群啊。