这段代码竟然会让 React 应用瘫痪?!
一、这段代码竟然会让 React 应用瘫痪
大家可以试试在自己的react应用控制台中运行下面的代码。
// react 17以下在控制台输入下面这段代码
const events = getEventListeners(document)
for (const eventName in events) {
const eventItems = events[eventName];
for (let i = 0, l = eventItems.length; i < l; i++) {
const eventItem = eventItems[i];
document.removeEventListener(eventItem.type,eventItem.listener);
}
}
// react 17及以上在在react应用的控制台输入下面的代码
const root = document.querySelector('#root');
const events = getEventListeners(root)
for (const eventName in events) {
const eventItems = events[eventName];
for (let i = 0, l = eventItems.length; i < l; i++) {
const eventItem = eventItems[i];
root.removeEventListener(eventItem.type,eventItem.listener);
}
}
运行完代码,试着操作页面,你会发现页面竟然不听使唤了,怎么操作都没反应。那这是为什么呢?在了解为什么之前,我们先来看看刚在控制台输入的代码的作用是什么: getEventListeners(dom)返回在指定dom上注册事件的监听器。该函数返回一个对象(包含了当前dom上注册的所有事件),所以这段代码的作用就是移除当前dom上所有已注册的事件监听。
那为什么移除了事件react应用就瘫痪了呢。因为react实现了自己的事件系统,所有的 React 事件都是通过这个事件系统来执行的,而这个事件系统是基于事件代理实现的,上面的代码相当于移除了代理事件的监听,再去操作页面自然就不会执行了。
二、为什么react要自己实现事件系统?
1、为了抹平不同浏览器事件对象间的差异。 2、统一管理事件,节约内存,提升性能。 3、跨端复用。 4、为事件分配优先级。 5、更容易添加新特性
以下是chatGPT的回答
为什么上面的代码分为了两个版本?
因为在 React17 之前事件代理是注册在 document
上的, React 17 及以后,React 将不再向 document
附加事件处理器。而会将事件代理注册到渲染 React 树的根 DOM 容器中,比如:下面的 id 为root 的 dom 容器:
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
为什么 React17 之后要进行改动?
React 团队为了能够让 React 渐进升级,提出了多版本共存的升级方案,也就是一个 React 应用中可能会有多个 React 版本。
如果页面上有多个 React 版本,他们都将在顶层注册事件处理器。这会破坏
e.stopPropagation()
:如果嵌套树结构中阻止了事件冒泡,但外部树依然能接收到它。这会使不同版本 React 嵌套变得困难重重。
以上引自 React 官方文档——传送门。多个 React 共存事件都注册在 document 上,会破坏原有的事件系统,出现不符合预期的问题,这就是进行改动的原因。
三、react事件系统的组成。
react的事件系统由两部分组成: 1、SyntheticEvent(合成事件) 合成事件是对浏览器原生事件对象的一层封装,兼容主流浏览器与浏览器原生事件有相同的api,比如stopPropagation。
2、模拟实现事件的传播机制 利用事件委托,并基于fiber架构实现了事件的“捕获、目标、冒泡”流程,并给不同的事件加上了不同的优先级。
四、上代码
1、在createRoot方法利用事件代理给根容器绑定事件,具体方法为listenToAllSupportedEvents,接收的参数为root根容器。allNativeEvents是所有待绑定的事件,listenToAllSupportedEvents将所有的事件都绑定到了root容器上,除了selectionchange,它绑定到了document上。
listenToNativeEvent方法主要决定了当前事件是否要捕获,绑定实现在addTrappedEventListener。最终执行了addEventListener。
下面来看看事件的优先级是怎么设置的,具体逻辑在createEventListenerWrapperWithPriority。
react事件的优先级有三种,分别是DiscreteEventPriority(优先级最高)代表事件为click,ContinuousEventPriority(优先级次一点)代表事件mousemove,DefaultEventPriority(默认优先级),最终会和lane模型的优先级对应起来进行调度
createEventListenerWrapperWithPriority返回的函数,也就是事件触发会触发的函数,最终会执行dispatchEvent
到这里全部事件的监听就完成了。
下面来看看触发一个click事件会发生什么,注意:会触发两次dispatchEvent 一次冒泡,一次捕获
首先会触发监听函数dispatchEvent,最后在批量更新函数中执行dispatchEventsForPlugins
我们来看看batchedUpdates干了什么?
在batchedUpdates中 将执行上下文置为 批量 并执行dispatchEventsForPlugins。
dispatchEventsForPlugins主要干了两件事,一个是 extractEvents 提取事件回调并生成合成事件,先看看提取事件回调。
那事件是如何收集的呢————通过事件名和与触发事件的 dom 对应的 FiberNode 节点上的事件名(比如:onClick)进行匹配(事件在 FIberNode 上表现为 key-value,也就是对象的一个属性值,比如点击事件:onClick: () => { console.log('这是点击事件对应的回调') }),匹配成功则将对应的事件回调加入 listeners 数组,然后再通过 return 找到父 FiberNode 继续匹配收集回调,一直向上冒泡收集到根节点为止。具体代码如下:
下面是合成事件的实现
,另一个是执行事件回调。
执行事件回调,如果是捕获阶段反着执行,反之顺着执行,模拟了事件的传播机制
最终事件回调在这里执行。
五、让瘫痪的react应用恢复
利用上面的原理,对 FiberNode 上的事件进行处理即可
const root = document.querySelector('#root');
const eventList = [
'click'
];
eventList.forEach(eventName =>{
root.addEventListener(eventName,listener,false);
})
function listener(e) {
// 拿到原生dom
const dom = e.target;
const eventType = e.type; // 当前的事件类型
for(let key in dom) {
if(key.includes('__reactFiber$')){
// 获取fiber节点
console.log(e.target[key])
const fiber = e.target[key];
const { listeners, captureListeners } = accumulateSinglePhaseListeners(fiber, eventType)
// 执行冒泡回调
for(let i = 0, l = listeners.length; i < l;i++){
listeners[i](e);
}
// 执行捕获回调
for(let i = captureListeners.length - 1; i >= 0 ;i ++){
captureListeners[i](e);
}
}
if(key.includes('__reactProps$')){
// 获取props
console.log(e.target[key])
}
}
}
// 这里实现为了方便把捕获和冒泡一起处理了,实际实现不是这样
// 收集回调
function accumulateSinglePhaseListeners(fiber, eventType, listeners = [], captureListeners = []) {
const eventTypeName = eventType.charAt(0).toUpperCase() + eventType.slice(1);
let curFiber = fiber;
// 组装事件名称
const eventName = 'on' + eventTypeName;
const capEventName = 'onCapture' + eventTypeName;
while(curFiber !== null) {
const props = fiber.memoizedProps;
if(!props) continue
// 获取事件回调函数
const listener = props[eventName];
const captureListener = props[capEventName];
if(listener) {
listeners.push(listener);
}
if(captureListener) {
captureListeners.push(captureListener);
}
curFiber = curFiber.return;
}
return {
listeners,
captureListeners
}
}
最后
感谢大家的阅读,有不对的地方也欢迎大家指出来。
参考资料
React设计原理-卡颂 React18.2.0源码 chatGPT React 官网
转载自:https://juejin.cn/post/7229186336153059387