React合成事件机制

38 阅读8分钟

什么是React合成事件

React 事件合成(SyntheticEvent)是 React 框架对浏览器原生事件系统的一层封装,核心目的是为了统一跨浏览器的事件行为优化性能

简单来说,React 并没有直接使用浏览器原生的 Event 对象,而是在原生事件触发后,创建了一个属于 React 自己的事件对象(即 SyntheticEvent),并将其传递给你的事件处理函数。

为什么要有事件合成机制

  1. 跨浏览器兼容性:不同浏览器(尤其是古老的 IE)对原生事件的实现存在诸多差异。例如,获取事件目标元素,标准浏览器用 event.target,而 IE 用 event.srcElement。React 事件合成层会屏蔽这些底层差异,为你提供一个统一、一致的 API。你只需要使用 e.target,React 会在内部处理好不同浏览器的兼容问题。

  2. 性能优化:React 采用了 事件委托(Event Delegation) 策略。

  • 不是在每个需要响应事件的 DOM 元素上都直接绑定事件监听器。
  • 而是将所有事件监听器都委托给了组件树的顶层容器(通常是 document)。
  • 当一个事件在 DOM 中触发并冒泡到顶层时,React 会捕获这个事件,然后根据事件的类型和目标 DOM 元素,在自己的虚拟 DOM 树中找到对应的组件,并执行该组件上注册的事件处理函数。
  • 这种方式大大减少了页面中注册的事件监听器数量,从而提升了渲染性能和内存使用效率,尤其是在拥有大量元素的复杂应用中。
  1. 事件对象池化(Event Pooling) :为了进一步提升性能,减少垃圾回收(Garbage Collection)的压力,React 会对 SyntheticEvent 对象进行池化复用。
  • 当一个事件被触发时,React 会从事件池中取出一个 SyntheticEvent 对象,设置其内部属性(如 typetargetnativeEvent 等),然后将其传递给你的处理函数。
  • 当事件处理函数执行完毕后,React 会清空这个 SyntheticEvent 对象的属性,并将其放回事件池中,等待下一次事件触发时再次使用。

React中事件合成机制是如何实现的

一、事件注册:委托机制

React 并不会在真实 DOM 元素上直接绑定事件监听器,而是采用了事件委托(Event Delegation)策略。

  1. 统一挂载到顶层容器
  • ReactDOM.render 会创建一个根容器(如 div#root)。
    • 16.x 及之前所有事件委托到 document ,多 React 应用共存时事件可能冲突
    • 17.x 及之后委托到应用根 DOM 节点,隔离不同 React 版本的事件系统,避免全局污染
  • React 将所有支持的事件(如 clickinputchange 等)的监听器统一挂载到这个根容器上,而不是每个 DOM 元素都单独绑定。
  • 这样做的好处是减少了大量的 DOM 事件绑定操作,提高了性能。
  1. 事件类型映射
  • React 内部维护了一个事件类型的映射表,比如将 onClick 映射到原生的 click 事件。
  • 当你在 JSX 中写 onClick={handleClick} 时,React 并不会直接在 DOM 上绑定 click 事件,而是将这个事件处理函数存储起来,等待事件触发。

二、事件触发:捕获与分发

当一个原生 DOM 事件触发时,会经历以下流程:

  1. 原生事件捕获
  • 浏览器原生事件会从事件源向上冒泡(或捕获)到 React 的顶层容器。
  • 顶层容器上的 React 事件监听器会捕获到这个原生事件。
  1. 创建合成事件对象
  • React 会根据原生事件的类型,从事件对象池中取出一个 SyntheticEvent 实例。
  • SyntheticEvent 会封装原生事件的属性(如 targettypebubbles 等),并提供统一的 API(如 stopPropagation()preventDefault())。
  • 这样做是为了跨浏览器兼容性,屏蔽不同浏览器原生事件的差异。
  1. 事件分发
  • React 会根据事件源的 DOM 元素,找到对应的 React 组件实例。
  • 然后,React 会模拟一个事件冒泡(或捕获)的过程,从最底层的组件开始,向上触发所有注册的事件处理函数。
  • 在这个过程中,你可以调用 e.stopPropagation() 来阻止事件继续向上冒泡。
  1. 合成事件中进行捕获阶段监听
  • 在 React 中,你不能像在原生 DOM 中那样直接通过 addEventListener(event, handler, true) 来指定事件在捕获阶段触发。React 的合成事件系统默认是在冒泡阶段处理事件的。
  • 但是,React 提供了一个特殊的语法来监听捕获阶段的事件:在事件名后面加上 Capture 后缀
  • 将你想要监听的事件名(如 onClick)改为 onClickCapture,React 就会在事件的捕获阶段触发你的处理函数。

三、事件对象池化

为了减少内存开销和垃圾回收的压力,React 对 SyntheticEvent 对象进行了池化复用

  1. 对象池的工作原理

    • React 维护了一个 SyntheticEvent 的对象池,里面缓存了大量的 SyntheticEvent 实例。
    • 当事件触发时,React 会从池中取出一个实例,设置其属性(如 nativeEventtarget 等),然后传递给事件处理函数。
    • 当事件处理函数执行完毕后,React 会将该实例的属性清空,然后放回池中,等待下次复用。
  2. 使用注意事项

    • 由于事件对象被池化复用,在异步操作中直接访问 SyntheticEvent 对象会导致数据丢失
    • 如果需要在异步操作中使用事件数据,可以通过 e.persist() 方法将事件对象从池中取出,使其不会被复用。
function handleClick(e) {
  e.persist(); // 将事件对象从池中取出
  setTimeout(() => {
    console.log(e.target); // 此时可以安全访问
  }, 1000);
}

四、合成事件与原生事件的关系

  1. 合成事件是对原生事件的封装
  • SyntheticEvent 内部持有一个 nativeEvent 属性,指向浏览器原生的事件对象。
  • 你可以通过 e.nativeEvent 来访问原生事件的所有属性和方法。
  1. 事件冒泡的区别
  • 合成事件的冒泡是 React 模拟的,只在 React 组件树中生效。
  • 如果你在原生事件中调用 e.stopPropagation(),会阻止事件冒泡到 React 的顶层容器,从而导致合成事件无法触发。
  • 反之,在合成事件中调用 e.stopPropagation(),只会阻止 React 组件树内部的事件冒泡,不会影响原生事件的冒泡。

总结

React 的事件合成机制可以概括为以下几个步骤:

  1. 事件注册:将所有事件监听器委托给顶层容器。
  2. 事件触发:原生事件冒泡到顶层容器,被 React 捕获。
  3. 创建合成事件:从事件池中取出一个 SyntheticEvent 实例,封装原生事件。
  4. 事件分发:在 React 组件树中模拟事件冒泡,触发对应的事件处理函数。
  5. 事件回收:事件处理函数执行完毕后,将 SyntheticEvent 实例清空并放回池中。

这种机制的好处是:

  • 跨浏览器兼容性:屏蔽了不同浏览器原生事件的差异。
  • 性能优化:减少了 DOM 事件绑定的数量,通过对象池化减少了内存开销。
  • 统一的 API:提供了一致的事件处理接口,方便开发者使用。

执行顺序分析


<html>
    <body>
        <div id="root">
            <!-- React 的根容器 -->
            <div class="grandparent">
                <!-- React 组件 A -->
                <div class="parent"> 
                <!-- React 组件 B --> 
                    <button class="child">点击我</button> 
                <!-- React 组件 C --> 
                </div> 
            </div> 
        </div> 
    </body> 
</html>
// 当用户点击了 .child 按钮后...

// ========================================================
// 阶段 1: 原生事件捕获阶段 (从 window 向下到目标元素)
// ========================================================
console.log('--- 原生捕获阶段开始 ---');
nativeListenerHtmlCapture();     // 1. html 捕获
nativeListenerBodyCapture();     // 2. body 捕获
nativeListenerRootCapture();     // 3. #root 捕获
nativeListenerGrandparentCapture(); // 4. .grandparent 捕获
nativeListenerParentCapture();   // 5. .parent 捕获
nativeListenerChildCapture();    // 6. .child 捕获

// ========================================================
// 阶段 2: 原生事件目标阶段 (事件到达触发点)
// ========================================================
console.log('--- 原生目标阶段开始 ---');
nativeListenerChildBubble();     // 7. .child 冒泡(目标元素本身)

// ========================================================
// 阶段 3: 原生事件冒泡阶段 (从目标元素向上到 window)
// ========================================================
console.log('--- 原生冒泡阶段开始 ---');
nativeListenerParentBubble();    // 8. .parent 冒泡
nativeListenerGrandparentBubble(); // 9. .grandparent 冒泡

// 10. #root 冒泡(React 在此处介入)
nativeListenerRootBubble(); 
{
  console.log('--- React 合成事件机制介入 ---');
  
  // a. React 从原生事件中创建 SyntheticEvent 对象
  // b. 先执行合成事件的捕获阶段(从顶层组件向下)
  console.log('--- React 合成事件捕获阶段开始 ---');
  reactHandlerACapture(syntheticEvent); // A 组件捕获
  reactHandlerBCapture(syntheticEvent); // B 组件捕获
  reactHandlerCCapture(syntheticEvent); // C 组件捕获
  console.log('--- React 合成事件捕获阶段结束 ---');
  
  // c. 再执行合成事件的冒泡阶段(从目标组件向上)
  console.log('--- React 合成事件冒泡阶段开始 ---');
  reactHandlerC(syntheticEvent);        // C 组件冒泡
  reactHandlerB(syntheticEvent);        // B 组件冒泡
  reactHandlerA(syntheticEvent);        // A 组件冒泡
  console.log('--- React 合成事件冒泡阶段结束 ---');
  
  // d. 合成事件处理完毕,将 SyntheticEvent 放回事件池
}

nativeListenerBodyBubble();      // 11. body 冒泡(React 处理完成后继续)
nativeListenerHtmlBubble();      // 12. html 冒泡
// window 冒泡(若有)

console.log('--- 所有事件处理完毕 ---');