上篇文档 React事件机制 - 源码概览(上)说到了事件执行阶段的构造合成事件部分,本文接着继续往下分析
批处理合成事件
入口是 runEventsInBatch
// runEventsInBatch
// packages/events/EventPluginHub.js
export function runEventsInBatch(
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
simulated: boolean,
) {
if (events !== null) {
eventQueue = accumulateInto(eventQueue, events);
}
const processingEventQueue = eventQueue;
eventQueue = null;
if (!processingEventQueue) {
return;
}
if (simulated) {
// react-test 才会执行的代码
// ...
} else {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}
这个方法首先会将当前需要处理的 events
事件,与之前没有处理完毕的队列调用 accumulateInto
方法按照顺序进行合并,组合成一个新的队列,因为之前可能就存在还没处理完的合成事件,这里就又有得到执行的机会了
如果合并后的队列为 null
,即没有需要处理的事件,则退出,否则根据 simulated
来进行分支判断调用对应的方法,这里的 simulated
标志位,字面意思是 仿造的、假装的
,其实这个字段跟 react-test
,即测试用例有关,只有测试用例调用 runEventsInBatch
方法的时候, simulated
标志位的值才为true
,除了这个地方以外,React
源码中还有其他的很多地方都会出现 simulated
,都是跟测试用例有关,看到了不用管直接走 else
逻辑即可,所以我们这里就走 else
的逻辑,调用 forEachAccumulated
方法
// packages/events/forEachAccumulated.js
function forEachAccumulated<T>(
arr: ?(Array<T> | T),
cb: (elem: T) => void,
scope: ?any,
) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
}
这个方法就是先看下事件队列processingEventQueue
是不是个数组,如果是数组,说明队列中不止一个事件,则遍历队列,调用 executeDispatchesAndReleaseTopLevel
,否则说明队列中只有一个事件,则无需遍历直接调用即可
所以来看下 executeDispatchesAndReleaseTopLevel
这个方法:
// packages/events/EventPluginHub.js
const executeDispatchesAndReleaseTopLevel = function(e) {
return executeDispatchesAndRelease(e, false);
};
// ...
const executeDispatchesAndRelease = function(
event: ReactSyntheticEvent,
simulated: boolean,
) {
if (event) {
executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
executeDispatchesAndReleaseTopLevel
又调用了 executeDispatchesAndRelease
,然后 executeDispatchesAndRelease
这个方法先调用了 executeDispatchesInOrder
,这个方法是事件处理的核心所在:
// packages/events/EventPluginUtils.js
// executeDispatchesInOrder
export function executeDispatchesInOrder(event, simulated) {
const dispatchListeners = event._dispatchListeners;
const dispatchInstances = event._dispatchInstances;
if (__DEV__) {
validateEventDispatches(event);
}
if (Array.isArray(dispatchListeners)) {
for (let 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;
}
首先对拿到的事件上挂在的 dispatchListeners
,也就是之前拿到的当前元素以及其所有父元素上注册的事件回调函数的集合,遍历这个集合,如果发现遍历到的事件的 event.isPropagationStopped()
为 true
,则遍历的循环直接 break
掉,这里的 isPropagationStopped
在前面已经说过了,它是用于标识当前 React Node
上触发的事件是否执行了 e.stopPropagation()
这个方法,如果执行了,则说明在此之前触发的事件已经调用 event.stopPropagation()
,isPropagationStopped
的值被置为 functionThatReturnsTrue
,即执行后为 true
,当前事件以及后面的事件作为父级事件就不应该再被执行了
这里当 event.isPropagationStopped()
为 true
时,中断合成事件的向上遍历执行,也就起到了和原生事件调用 stopPropagation
相同的效果
如果循环没有被中断,则继续执行 executeDispatch
方法,这个方法接下来又一层一层地掉了很多方法,最终来到 invokeGuardedCallbackImpl
:
// packages/shared/invokeGuardedCallbackImpl.js
let invokeGuardedCallbackImpl = function<A, B, C, D, E, F, Context>(
name: string | null,
func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
context: Context,
a: A,
b: B,
c: C,
d: D,
e: E,
f: F,
) {
const funcArgs = Array.prototype.slice.call(arguments, 3);
try {
func.apply(context, funcArgs);
} catch (error) {
this.onError(error);
}
};
关键在于这一句:
func.apply(context, funcArgs);
funcArgs
是什么呢?其实就是合成事件对象,包括原生浏览器事件对象的基本上所有属性和方法,除此之外还另外挂载了额外其他一些跟 React
合成事件相关的属性和方法,而 func
则就是传入的事件回调函数,对于本示例来说,就等于clickHandler
这个回调方法:
// func === clickHandler
clickHandler(e) {
console.log('click callback', e)
}
将 funcArgs
作为参数传入 func
,也即是传入 clickHandler
,所以我们就能够在 clickHandler
这个函数体内拿到 e
这个回调参数,也就能通过这个回调参数拿到其上面挂载的任何属性和方法,例如一些跟原生浏览器对象相关的属性和方法,以及原生事件对象本身(nativeEvent
)
至此,事件执行完毕
这个过程流程图如下:
事件清理
事件执行完毕之后,接下来就是一些清理工作了,因为 React
采用了对象池的方式来管理合成事件,所以当事件执行完毕之后就要清理释放掉,减少内存占用,主要是执行了上面提到过的位于 executeDispatchesAndRelease
方法中的 event.constructor.release(event);
这一句代码
这里面的 release
就是如下方法:
// packages/events/SyntheticEvent.js
function releasePooledEvent(event) {
const EventConstructor = this;
invariant(
event instanceof EventConstructor,
'Trying to release an event instance into a pool of a different type.',
);
event.destructor();
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event);
}
}
这个方法主要做了两件事,首先释放掉 event
上属性占用的内存,然后把清理后的 event
对象再放入对象池中,可以被后续事件对象二次利用
event.destructor();
这句就是用于释放内存的,destructor
这个方法的字面意思是 析构
,也就表示它是一个析构函数,了解 C/C++
的人应该对这个名词很熟悉,它一般都是用于 清理善后的工作,例如释放掉构造函数申请的内存空间以释放内存,这里的 destructor
方法同样是有着这个作用
destructor
是 SyntheticEvent
上的方法,所以所有的合成事件都能拿到这个方法:
// packages/events/SyntheticEvent.js
destructor: function() {
const Interface = this.constructor.Interface;
for (const propName in Interface) {
if (__DEV__) {
Object.defineProperty(
this,
propName,
getPooledWarningPropertyDefinition(propName, Interface[propName]),
);
} else {
this[propName] = null;
}
}
this.dispatchConfig = null;
this._targetInst = null;
this.nativeEvent = null;
this.isDefaultPrevented = functionThatReturnsFalse;
this.isPropagationStopped = functionThatReturnsFalse;
this._dispatchListeners = null;
this._dispatchInstances = null;
// 以下省略部分代码
// ...
}
JavaScript
引擎有自己的垃圾回收机制,一般来说不需要开发者亲自去回收内存空间,但这并不是说开发者就完全无法影响这个过程了,常见的手动释放内存的方法就是将对象置为 null
,destructor
这个方法主要就是做这件事情,遍历事件对象上所有属性,并将所有属性的值置为 null
总结
React
的事件机制看起来还是比较复杂的,我自己看了几遍源码又对着调试了几遍,现在又写了分析文章,回头再想想其实主线还是比较明确的,过完了源码之后,再去看 react-dom/src/events/ReactBrowserEventEmitter.js
这个源码文件开头的那一段图形化注释,整个流程就更加清晰了
顺便分享一个看源码的技巧,如果某份源码,比如 React
这种,比较复杂,代码方法很多,很容易看着看着就乱了,那么就不要再干看着了,直接写个简单的例子,然后在浏览器上打断点,对着例子和源码一步步调试,弄明白每一步的逻辑和目的,多调试几次后,基本上就能抓到关键点了,后续再通读源码的时候,就会流畅很多了