本文使用的是react 15.6.1的代碼
我們先看一段代碼
import React, {Component} from "react";
import "./App.css";
class App extends Component {
componentDidMount() {
this.refs.div.addEventListener('click', function () {
alert('dom event');
})
}
componentClick(event) {
alert('react event');
event.stopPropagation();
}
render() {
return (
<div ref="div">
<button ref="button" onClick={this.componentClick.bind(this)}>
button
</button>
</div>
);
}
}
export default App;
當(dāng)我們點(diǎn)擊按鈕的時(shí)候,會(huì)彈出哪一些框?
答案是分別彈出 dom event以及react event,如果只了解dom事件的童鞋也許會(huì)問,在componentClick中明明寫了stopPropagation,事件應(yīng)該不會(huì)冒泡到父元素了?。繉?duì)此有疑惑的童鞋可以看看下面的分析,幫助你更進(jìn)一步了解react的事件系統(tǒng)。
事件的注冊(cè)
熟悉react的同學(xué)應(yīng)該知道,在react中,幾乎所有的事件都被代理到了document之上,以達(dá)到優(yōu)化的目的,試想,如果react組件又1000個(gè)列表,每個(gè)列表都給他綁定一個(gè)click事件是多么費(fèi)性能的事情,即便不用react,我們也會(huì)使用代理來處理這樣綁定事件過多對(duì)性能的開銷。既然事件大部分都綁定到了document上,所以stopPropagation沒有起到相應(yīng)的作用,我們來看看是如何綁定的;
在ReactDomComponent.js中有這樣一段代碼,react組件渲染props的時(shí)候,發(fā)現(xiàn)如果是事件屬性,則會(huì)調(diào)用這樣一個(gè)函數(shù)enqueuePutListener
function enqueuePutListener(inst, registrationName, listener, transaction) {
if (transaction instanceof ReactServerRenderingTransaction) {
return;
}
//實(shí)體對(duì)象中dom節(jié)點(diǎn)信息
var containerInfo = inst._hostContainerInfo;
var isDocumentFragment =
containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
/**
* ownerDocument:該節(jié)點(diǎn)的頂層document
* [ownerDocument](https://developer.mozilla.org/zh-CN/docs/Web/API/Node/ownerDocument)
*/
var doc = isDocumentFragment
? containerInfo._node
: containerInfo._ownerDocument;
//注冊(cè)事件
listenTo(registrationName, doc);
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener,
});
}
在這段代碼中,簡(jiǎn)單做了這樣一件事情,首先獲取到真實(shí)節(jié)點(diǎn),判斷節(jié)點(diǎn)是否是document,如果不是,則將其節(jié)點(diǎn)的頂層document以及事件名傳入listenTo方法,對(duì)應(yīng)的listenTo在ReactBrowserEventEmitter.js中,如下
listenTo: function(registrationName, contentDocumentHandle) {
// 事件掛載節(jié)點(diǎn),大部分是document
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
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)) {
//注冊(cè)冒泡事件
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
dependency,
topEventMapping[dependency],
mountAt,
);
}
isListening[dependency] = true;
}
}
},
在這個(gè)方法中,著重對(duì)不同瀏覽器捕獲與冒泡不同進(jìn)行兼容處理,同時(shí),對(duì)wheel scroll等事件進(jìn)行了特殊處理,調(diào)用trapCapturedEvent和trapBubbledEvent來注冊(cè)捕獲和冒泡事件,我們來看看trapBubbledEvent的實(shí)現(xiàn)
return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
topLevelType,
handlerBaseName,
handle,
);
代碼很簡(jiǎn)單,調(diào)用了ReactEventListener的trapBubbledEvent方法,可是ReactEventListener對(duì)象是從哪里來的呢?在【react源碼閱讀筆記(3)batchedUpdates與Transaction】介紹過React在啟動(dòng)后會(huì)調(diào)用inject方法注入一些對(duì)象,在ReactDefaultInjection.js中執(zhí)行了
ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);
因此,在默認(rèn)情況下 ReactBrowserEventEmitter.ReactEventListener即為注入的ReactEventListener對(duì)象
//ReactEventListener.trapBubbledEvent
trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(
element,
handlerBaseName,
//事件回調(diào),注意
ReactEventListener.dispatchEvent.bind(null, topLevelType),
);
}
這個(gè)方法只是簡(jiǎn)單判斷了一下元素是否存在,隨后調(diào)用EventListener.listen注冊(cè)事件,EventListener.js位于node_modules/fbjs/lib中
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);
}
};
}
}
代碼更加簡(jiǎn)單了,就是對(duì)不同瀏覽器的addEventListener做兼容處理,到這里我們終于找到了事件注冊(cè)的源頭
但是我們?cè)?strong>ReactEventListener.trapBubbledEvent發(fā)現(xiàn)了,綁定到dom上的事件回調(diào),并不是我們?cè)趓eact寫的對(duì)應(yīng)的回調(diào)函數(shù),而是ReactEventListener.dispatchEvent.bind(null, topLevelType),這個(gè)又是什么呢?
// topLevelType:帶top的事件名,如topClick。不用糾結(jié)為什么帶一個(gè)top字段,知道它是事件名就OK了
// nativeEvent: 用戶觸發(fā)click等事件時(shí),瀏覽器傳遞的原生事件
dispatchEvent: function(topLevelType, nativeEvent) {
if (!ReactEventListener._enabled) {
return;
}
var bookKeeping = TopLevelCallbackBookKeeping.getPooled(
topLevelType,
nativeEvent,
);
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
簡(jiǎn)單總結(jié)就是
- 對(duì)dom組件進(jìn)行遍歷,如果是事件的話調(diào)用 enqueuePutListener
- 對(duì)該dom找到其document,綁定對(duì)應(yīng)的事件名
- document不管注冊(cè)的是什么事件,具有統(tǒng)一的回調(diào)函數(shù)handleTopLevelImpl
事件存儲(chǔ)
那么,真正的事件回調(diào)在哪里呢?
回到 enqueuePutListener方法,在listenTo后面執(zhí)行了方法enqueue,把真正的事件綁定回調(diào)listener最為參數(shù)傳入putListener函數(shù)中,也就是說,在ReactReconcileTransaction事務(wù)的close階段執(zhí)行notifyAll的時(shí)候就會(huì)調(diào)用putListener方法,
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener,
});
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(
listenerToPut.inst,
listenerToPut.registrationName,
listenerToPut.listener,
);
}
在這里,調(diào)用了 EventPluginHub.putListener方法,將對(duì)應(yīng)的組件對(duì)象,事件名以及回調(diào)作為參數(shù)傳遞
putListener: function(inst, registrationName, listener) {
// return '.' + inst._rootNodeID;
var key = getDictionaryKey(inst);
// 將所綁定的事件存入 listenerBank中,
var bankForRegistrationName =
listenerBank[registrationName] || (listenerBank[registrationName] = {});
// 將對(duì)應(yīng)的事件回調(diào)放入 bankForRegistrationName中,通過id區(qū)分不同組件當(dāng)時(shí)相同的事件,如組件A和B都有click事件,通過id進(jìn)行區(qū)分
bankForRegistrationName[key] = listener;
var PluginModule =
EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
PluginModule.didPutListener(inst, registrationName, listener);
}
}
在這里,react將不同時(shí)間存入listenerBank數(shù)組中,然后通過NodeId區(qū)分不同節(jié)點(diǎn)具體所綁定的事件
事件執(zhí)行
上面說到,react中會(huì)將真正的事件回調(diào)函數(shù)通過不同id,塞入listenerBank中,大部分事件都是綁定到document上,并使用dispatchEvent進(jìn)行分發(fā)事件,最后調(diào)用了handleTopLevelImpl,那么來看看handleTopLevelImpl事件做了些什么
function handleTopLevelImpl(bookKeeping) {
// 獲取這個(gè)event的真是dom元素,如果是text節(jié)點(diǎn),則返回父容器
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// 獲取事件節(jié)點(diǎn)所對(duì)應(yīng)的react實(shí)例
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
nativeEventTarget,
);
var ancestor = targetInst;
// 將該組件所有父節(jié)點(diǎn)放入ancestors,因?yàn)槭录赡軙?huì)改變父節(jié)點(diǎn)結(jié)構(gòu),因此在執(zhí)行事件回調(diào)之前緩存當(dāng)前parent
do {
bookKeeping.ancestors.push(ancestor);
ancestor = ancestor && findParent(ancestor);
} while (ancestor);
for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
// 從當(dāng)前組件開始執(zhí)行注冊(cè)的事件,模擬冒泡操作
ReactEventListener._handleTopLevel(
bookKeeping.topLevelType,
targetInst,
bookKeeping.nativeEvent,
getEventTarget(bookKeeping.nativeEvent),
);
}
}
從源碼中看出handleTopLevelImpl主要是模擬了事件的冒泡操作,拿到事件的注冊(cè)對(duì)象,然后依次找到其父級(jí)組件,調(diào)用ReactEventListener._handleTopLeve
ReactEventListener._handleTopLeve本質(zhì)上是調(diào)用ReactBrowserEventEmitter.handleTopLevel的方法,該代碼位于ReactEventEmitterMixin中
handleTopLevel: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
//合成對(duì)應(yīng)的事件
var events = EventPluginHub.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
//將事件放入隊(duì)列中
runEventQueueInBatch(events);
},
在這里,我們發(fā)現(xiàn),這里會(huì)將組建實(shí)例,具體事件對(duì)象等參數(shù)調(diào)用extractEvents合成react的事件對(duì)象,來看看react將事件是如何合成的
事件合成
EventPluginHub.extractEvents方法
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
var events;
// 獲取事件插件,
var plugins = EventPluginRegistry.plugins;
for (var i = 0; i < plugins.length; i++) {
// 通過各個(gè)插件返回事件數(shù)組,最后將事件數(shù)組合并返回
var possiblePlugin = plugins[i];
if (possiblePlugin) {
var extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
//該方法比較簡(jiǎn)單,就是,簡(jiǎn)單來說就是數(shù)組合并,
// 如果param1和2都是數(shù)組,那么調(diào)用concat合并
// 如果其中有一個(gè)是對(duì)象,將其按順序插入新的數(shù)組之中,
// 最后返回的一定是數(shù)組
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
這段代碼依然相對(duì)簡(jiǎn)單,合成事件的時(shí)候,會(huì)依次通過EventPluginRegistry.plugins插件列表來生成對(duì)應(yīng)的事件數(shù)組,最后將這個(gè)生成的事件合并為一個(gè)數(shù)組返回,
我們來看看具體對(duì)應(yīng)的插件是什么
在我們的老朋友ReactDefaultInjection中,會(huì)自動(dòng)注冊(cè)5個(gè)插件
ReactInjection.EventPluginHub.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
我們來分析其中一個(gè)SimpleEventPlugin插件的extractEvents方法
extractEvents: function(
topLevelType: TopLevelTypes,
targetInst: ReactInstance,
nativeEvent: MouseEvent,
nativeEventTarget: EventTarget,
): null | ReactSyntheticEvent {
var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
return null;
}
var EventConstructor;
switch (topLevelType) {
//.....
case 'topCopy':
case 'topCut':
case 'topPaste':
EventConstructor = SyntheticClipboardEvent;
break;
}
// 從緩存池中獲取對(duì)應(yīng)的event
var event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
//最后實(shí)際調(diào)用了ReactDOMTraversal中的traverseTwoPhase方法
mao.accumulateTwoPhaseDispatches(event);
return event;
},
function traverseTwoPhase(inst, fn, arg) {
//這里的fn是EventPropagators中的accumulateDirectionalDispatches方法
var path = [];
// 依次尋找父節(jié)點(diǎn)
while (inst) {
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);
}
}
在這里對(duì)模擬進(jìn)行了冒泡與捕獲,同時(shí)調(diào)用了accumulateDirectionalDispatches方法,來看看fn中處理了什么
function accumulateDispatches(inst, ignoredDirection, event) {
if (event && event.dispatchConfig.registrationName) {
var registrationName = event.dispatchConfig.registrationName;
var listener = getListener(inst, registrationName);
if (listener) {
// 將具體的event以及實(shí)例放入event下面的數(shù)組之中
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
}
參考上文,也就是說,合成的event保存了綁定事件節(jié)點(diǎn)到其父節(jié)點(diǎn)/實(shí)例的所有l(wèi)istener以及inst,但是我們還沒有看到具體的作用,我們繼續(xù)分析。
事件分發(fā)
事件已經(jīng)由插件合成完畢,我們回到 handleTopLevel方法,看看 runEventQueueInBatch(events)具體做了些什么?
function runEventQueueInBatch(events) {
// 先將events事件放入隊(duì)列中,不做分析,類似push
EventPluginHub.enqueueEvents(events);
//觸發(fā)該事件隊(duì)列中的所有事件
EventPluginHub.processEventQueue(false);
}
來看看processEventQueue方法
processEventQueue: function(simulated) {
// 獲取已合成事件隊(duì)列
var processingEventQueue = eventQueue;
eventQueue = null;
if (simulated) {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseSimulated,
);
} else {
// 讓所有事件執(zhí)行executeDispatchesAndReleaseTopLevel方法,
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}
// This would be a good time to rethrow if any of the event handlers threw.
ReactErrorUtils.rethrowCaughtError();
}
往下分析
/**
*
* @param e 事件隊(duì)列中的某個(gè)
*/
var executeDispatchesAndReleaseTopLevel = function(e) {
return executeDispatchesAndRelease(e, false);
};
var executeDispatchesAndRelease = function(event, simulated) {
if (event) {
EventPluginUtils.executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
executeDispatchesAndReleaseTopLevel最后本質(zhì)是調(diào)用了EventPluginUtils.executeDispatchesInOrder(event, simulated)方法
function executeDispatchesInOrder(event, simulated) {
// 合成事件中的對(duì)象以及事件回調(diào)
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
for (var i = 0; i < dispatchListeners.length; i++) {
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;
}
這段代碼相對(duì)簡(jiǎn)單了,如果有處理這個(gè)事件的回調(diào)函數(shù),就調(diào)用executeDispatch進(jìn)行處理
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
// 獲取具體dom
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {
// 在15.6版本invokeGuardedCallbackWithCatch和下面的invokeGuardedCallback代碼是相同的,具體的代碼就是執(zhí)行l(wèi)istener,并將event作為參數(shù)
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
//
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
到這里,拿到真實(shí)的dom,將合成的event賦予currentTarget,是不是和原生了event有點(diǎn)神似,最后,執(zhí)行了所綁定的回調(diào)函數(shù),直到這里我們發(fā)現(xiàn)了最開始的問題,為什么我們?cè)?strong>componentClick中調(diào)用的stopPropagation并沒有干擾到dom的事件,因?yàn)檫@個(gè)時(shí)候我們拿到的event并不是真實(shí)dom的事件,是完全由react封裝實(shí)現(xiàn)了標(biāo)準(zhǔn)dom的虛擬事件。
總結(jié)
react為什么要做這么一套復(fù)雜的事件系統(tǒng)?用原生的不好嗎?
我覺得這么設(shè)計(jì)至少可以有(還有什么好處歡迎告訴我)
- 自己實(shí)現(xiàn)事件,可以有效的規(guī)避不同瀏覽器對(duì)事件的兼容性錯(cuò)誤。
- 全局事件代理,對(duì)系統(tǒng)優(yōu)化無疑的巨大的性能提升。
參考資料
[React源碼分析7 — React合成事件系統(tǒng)](http://blog.csdn.net/u013510838/article/details/612247600