likes
comments
collection
share

这段代码竟然会让 React 应用瘫痪?!

作者站长头像
站长
· 阅读数 34

一、这段代码竟然会让 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的回答

这段代码竟然会让 React 应用瘫痪?!

为什么上面的代码分为了两个版本?

因为在 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上。

这段代码竟然会让 React 应用瘫痪?!

这段代码竟然会让 React 应用瘫痪?!

这段代码竟然会让 React 应用瘫痪?! listenToNativeEvent方法主要决定了当前事件是否要捕获,绑定实现在addTrappedEventListener。最终执行了addEventListener。

这段代码竟然会让 React 应用瘫痪?!

这段代码竟然会让 React 应用瘫痪?! 下面来看看事件的优先级是怎么设置的,具体逻辑在createEventListenerWrapperWithPriority。 react事件的优先级有三种,分别是DiscreteEventPriority(优先级最高)代表事件为click,ContinuousEventPriority(优先级次一点)代表事件mousemove,DefaultEventPriority(默认优先级),最终会和lane模型的优先级对应起来进行调度 这段代码竟然会让 React 应用瘫痪?!

这段代码竟然会让 React 应用瘫痪?!

createEventListenerWrapperWithPriority返回的函数,也就是事件触发会触发的函数,最终会执行dispatchEvent

到这里全部事件的监听就完成了。


下面来看看触发一个click事件会发生什么,注意:会触发两次dispatchEvent 一次冒泡,一次捕获

首先会触发监听函数dispatchEvent,最后在批量更新函数中执行dispatchEventsForPlugins

这段代码竟然会让 React 应用瘫痪?!

我们来看看batchedUpdates干了什么?

这段代码竟然会让 React 应用瘫痪?! 在batchedUpdates中 将执行上下文置为 批量 并执行dispatchEventsForPlugins。 这段代码竟然会让 React 应用瘫痪?! dispatchEventsForPlugins主要干了两件事,一个是 extractEvents 提取事件回调并生成合成事件,先看看提取事件回调。

这段代码竟然会让 React 应用瘫痪?! 那事件是如何收集的呢————通过事件名和与触发事件的 dom 对应的 FiberNode 节点上的事件名(比如:onClick)进行匹配(事件在 FIberNode 上表现为 key-value,也就是对象的一个属性值,比如点击事件:onClick: () => { console.log('这是点击事件对应的回调') }),匹配成功则将对应的事件回调加入 listeners 数组,然后再通过 return 找到父 FiberNode 继续匹配收集回调,一直向上冒泡收集到根节点为止。具体代码如下:

这段代码竟然会让 React 应用瘫痪?!

这段代码竟然会让 React 应用瘫痪?! 下面是合成事件的实现

这段代码竟然会让 React 应用瘫痪?!

,另一个是执行事件回调。

这段代码竟然会让 React 应用瘫痪?!

这段代码竟然会让 React 应用瘫痪?! 这段代码竟然会让 React 应用瘫痪?! 执行事件回调,如果是捕获阶段反着执行,反之顺着执行,模拟了事件的传播机制

这段代码竟然会让 React 应用瘫痪?! 最终事件回调在这里执行。

五、让瘫痪的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
评论
请登录