主题
原理篇-事件原理(v18 新版本)
一 前言
老版本的事件原理有一个问题就是,捕获阶段和冒泡阶段的事件都是模拟的,本质上都是在冒泡阶段执行的。 如下例子, 老版本的事件系统: 事件监听 -> 捕获阶段执行 -> 冒泡阶段执行 而新版本的事件系统: 捕获阶段执行 -> 事件监听 -> 冒泡阶段执行
jsx
function Index() {
const refObj = React.useRef(null);
useEffect(() => {
const handler = () => {
console.log("事件监听");
};
refObj.current.addEventListener("click", handler);
return () => {
refObj.current.removeEventListener("click", handler);
};
}, []);
const handleClick = () => {
console.log("冒泡阶段执行");
};
const handleCaptureClick = () => {
console.log("捕获阶段执行");
};
return (
<button ref={refObj} onClick={handleClick} onClickCapture={handleCaptureClick}>
点击
</button>
);
}
二 事件绑定——事件初始化
在 React 新版的事件系统中,在 createRoot 会一口气向外层容器上注册完全部事件。 如果这个事件既可以冒泡,又可以捕获,则注册 2 次。否则就注册 1 次。
- allNativeEvents:allNativeEvents 是一个 set 集合,保存了 81 个浏览器常用事件。
- nonDelegatedEvents :这个也是一个集合,保存了浏览器中不会冒泡的事件,一般指的是媒体事件,比如 pause,play,playing 等,还有一些特殊事件,比如 cancel ,close,invalid,load,scroll 。
三 事件触发
dispatchEvent 保留核心的代码如下:
js
batchedUpdates(function () {
return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
});
dispatchEventsForPlugins 代码如下:
js
function dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
) {
/* 找到发生事件的元素——事件源 */
var nativeEventTarget = getEventTarget(nativeEvent);
/* 待更新队列 */
var dispatchQueue = [];
/* 找到待执行的事件 */
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags
);
/* 执行事件 */
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
extractEvents 会往 dispatchQueue 添加一些东东,发生点击事件,会触发 2 次 dispatchEvents,1 次在捕获阶段,1 次在冒泡阶段。 第一次打印:
第二次打印:
extractEvents 函数的逻辑如下:
js
var SyntheticEventCtor = SyntheticEvent;
/* 针对不同的事件,处理不同的事件源 */
switch (domEventName) {
case 'keydown':
case 'keyup':
SyntheticEventCtor = SyntheticKeyboardEvent;
break;
case 'focusin':
reactEventType = 'focus';
SyntheticEventCtor = SyntheticFocusEvent;
break;
....
}
/* 找到事件监听者,也就是我们 onClick 绑定的事件处理函数 */
var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
/* 向 dispatchQueue 添加 event 和 listeners */
if(_listeners.length > 0){
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
dispatchQueue.push({
event: _event,
listeners: _listeners
});
}
event 是通过事件插件合成的,是 React 自己创建的事件源对象。捕获阶段的 listener 只有一个,而冒泡阶段的 listener 有两个,这是因为 div button 上都有 onClick 事件。 _listeners 本质上也是一个对象,里面存放了三个属性:
- currentTarget:发生事件的 DOM 元素。
- instance : button 对应的 fiber 元素。
- listener :一个数组,存放绑定的事件处理函数本身
如上可以总结为: 当发生一次点击事件,React 会根据事件源对应的 fiber 对象,根据 return 指针向上遍历,收集所有相同的事件,比如是 onClick,那就收集父级元素的所有 onClick 事件,比如是 onClickCapture,那就收集父级的所有 onClickCapture。
得到了 dispatchQueue 之后,就需要 processDispatchQueue 执行事件了,这个函数的内部会经历两次遍历:
- 第一次遍历 dispatchQueue,通常情况下,只有一个事件类型,所以 dispatchQueue 中只有一个元素。
- 接下来会遍历每一个元素的 listener,执行 listener 的时候有一个特点: 如果是捕获阶段执行的函数,那么 listener 数组中函数,会从后往前执行,如果是冒泡阶段执行的函数,会从前往后执行,用这个模拟出冒泡阶段先子后父,捕获阶段先父后子。
如果一个事件中执行了 e.stopPropagation
,接下来就可以通过 event.isPropagationStopped
来判断是否阻止冒泡,如果阻止,那么就会退出。
js
/* 如果在捕获阶段执行。 */
if (inCapturePhase) {
for (var i = dispatchListeners.length - 1; i >= 0; i--) {
var _dispatchListeners$i = dispatchListeners[i],
instance = _dispatchListeners$i.instance,
currentTarget = _dispatchListeners$i.currentTarget,
listener = _dispatchListeners$i.listener;
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
/* 执行事件 */
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
for (var _i = 0; _i < dispatchListeners.length; _i++) {
var _dispatchListeners$_i = dispatchListeners[_i],
_instance = _dispatchListeners$_i.instance,
_currentTarget = _dispatchListeners$_i.currentTarget,
_listener = _dispatchListeners$_i.listener;
if (_instance !== previousInstance && event.isPropagationStopped()) {
return;
}
/* 执行事件 */
executeDispatch(event, _listener, _currentTarget);
previousInstance = _instance;
}
}