图解React合成事件系统+源码导读

1,607 阅读6分钟

前言

大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.

我的宗旨就是 万物皆可手写

今天我们来看一看React的合成事件模型,并尝试自己手写一个合成事件系统

新手创作不易,有问题欢迎指出和轻喷,谢谢


React合成事件

我们知道,react中的事件都是合成事件,什么是合成事件呢? 我们来看一段代码

  <button onClick={(e) => {
                console.log('React合成事件', e);
   }}>合成事件</button>

image.png

点击按钮后,我们获取到的事件对象e,是被react重新封装后的SyntheticBaseEvent, 而其中的nativeEvent则是我们的原生事件对象event

事件冒泡和捕获

我们知道,在浏览器中的事件会经过两个阶段 捕获阶段-冒泡阶段

  • 捕获阶段: 从document层层向下找到触发事件的Dom节点(button),获取到event对象
  • 冒泡阶段: event对象会层层上传,如果某一层的dom上监听了click事件,则会触发事件回调,传入event作为参数

image.png

所以如果我们有如下代码

<div id="div2">
    <div id="div1">
        <button id="btn"></button>
    </div>
</div>


const div1 = document.getElementById('div1')
const div2 = document.getElementById('div1')
const btn = document.getElementById('btn')

div1.addEventListener('click',()=>{ console.log('点击div1', e);})
div2.addEventListener('click',()=>{ console.log('点击div1', e);})
btn.addEventListener('click',()=>{ console.log('点击btn', e);})

// 点击后执行结果
// 点击button
// 点击div2
// 点击div1

// 如果我们给addEventListener设置了capture属性,那么会直接在捕获阶段执行回调

div1.addEventListener('click',()=>{ console.log('点击div1', e);},true)
div2.addEventListener('click',()=>{ console.log('点击div1', e);},true)
btn.addEventListener('click',()=>{ console.log('点击btn', e);},true)

// 点击后执行结果
// 点击div2
// 点击div1
// 点击button

事件代理

为了获得更加优异的性能,我们有时候会将button上的事件直接代理到上层dom节点处执行。

而React的逻辑就是,在顶层document处监听所有的事件,将事件代理到document上,然后手动模拟冒泡执行和捕获执行的逻辑

这样做的好处是除了document节点,底层的dom都不需要添加监听器了,那么就可以大大提高事件性能(跟事件代理一样的逻辑)。

React事件代理模型

  1. 如果我们要实现上述代码逻辑,需要给btn,div1,div2都设置一个事件监听器,并给出onClick回调
  2. react会直接在document上监听click事件,如果事件上浮到document时进行捕获
  3. 捕获到e后,通过e找到触发的dom节点(button),并按照路径将三个onClick回调收集起来
  4. 按照冒泡顺序1-2-3执行回调, (如果是onClickCapture事件,则会按照捕获顺序3-2-1执行回调) image.png

有的小伙伴可能会问如何收集dom上的事件呢?

我们知道,react会形成一个虚拟dom树(fiber树), 而每个fiber.return都保存了当前节点对应的父节点,我们即可通过这个属性沿着dom上浮路径访问到所有的fiber节点

而当我们写下<button onClick={fn}></button>时,就会给对应的fiber节点的props上添加onCilck属性,保存了回调fn

监听所有原生事件并包装为合成事件

  1. 我们在React.render(createRoot)的时候,在document上监听所有的原生事件,将所有事件代理起来。

  2. 捕获到event后,收集沿途的回调函数,判断isCapture(如果事件为onClickCapture),

  3. 包装为react的SyntheticBaseEvent后即可开始按顺序执行收集来的回调函数,并将其作为参数传入

源码导读解析

合成事件的源码不算复杂,这里使用调用栈的方式记录合成事件 大家可以打开react18的源码,一步步查找下去:

  1. 入口 ReactDom.createRoot()
ReactDom.createRoot()
 // 创建时执行,监听所有原生事件
listenToAllSupportedEvents(container);
  1. 监听 listenToAllSupportedEvents
export function listenToAllSupportedEvents(
  rootContainerElement: EventTarget
) {
  // 保证事件只注册一次
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;
    // set中记录所有原生事件名称
    allNativeEvents.forEach(domEventName => {
      if (domEventName !== 'selectionchange') {
        // 特殊事件不需要委托根节点:如 'cancel'、'load'、'scroll' 等
        if (!nonDelegatedEvents.has(domEventName)) {
          // 在根节点上绑定对此事件的冒泡阶段的事件委托
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        //  在根节点上绑定对此事件的捕获阶段的事件委托
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
  }
}
  1. 绑定事件 (委托阶段+冒泡阶段) listenToNativeEvent addTrappedEventListener
export function listenToNativeEvent(
  // 原生事件名
  domEventName: DOMEventName, 
  // true 捕获阶段,false,冒泡阶段
  isCapturePhaseListener: boolean, 
  // 添加事件委托的节点
  target: EventTarget,
): void {

  // ... 最终调用  
  addTrappedEventListener(...args);
}



function addTrappedEventListener(...args) {
  // 创建事件委托的回调函数
  let listener = createEventListenerWrapperWithPriority();
  let unsubscribeListener;
  // 根据捕获阶段还是冒泡阶段,会调用两个不同的函数
  if (isCapturePhaseListener) {
    unsubscribeListener = addEventCaptureListener(true,...args);
  } else {
    unsubscribeListener = addEventBubbleListener(false,...args);
  }
}
  1. 创建事件,并给出优先级 createEventListenerWrapperWithPriority
export function createEventListenerWrapperWithPriority(...args): Function {
  // 根据事件名获取优先级,一般的事件如 'click'、'input' 都是 DiscreteEventPriority 级别的
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  
  // 这个返回值就是我们的回调函数 listener 
  // listenerWrapper 其实接受四个参数,我们当前只绑定了前三个,
  // 第四个就是在触发事件的时候,调用回调传入的,
  // 也就是 DOM 的 Event 对象。
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

  1. 绑定dom事件
// 就是addEventListener  三号参数为true  捕获执行
export function addEventCaptureListener(): Function {
  target.addEventListener(eventType, listener, true);
  return listener;
}
// addEventListener  三号参数为false   冒泡执行 
export function addEventBubbleListener(): Function {
  target.addEventListener(eventType, listener, false);
  return listener;
}

6.执行事件-1 dispatchEvent

上面5步绑定好事件后, 我们click点击事件触发后会执行

dispatchDiscreteEvent()
dispatchContinuousEvent()
dispatchEvent() // 最终执行
// 下层调用
attemptToDispatchEvent() 
// 下层调用
dispatchEventForPluginEventSystem()
// 下层调用
dispatchEventsForPlugins()

7.执行事件-2 dispatchEventsForPlugins

这里做了两件事

  • 收集所有的事件(按照冒泡/捕获顺序排序)

冒泡顺序:从底层dom到顶层dom 捕获顺序:从顶层dom到底层dom

  • 依次触发事件
function dispatchEventsForPlugins(...args): void {
  
  // 拿到当前点击的 DOM 节点
  const nativeEventTarget = getEventTarget(nativeEvent);
  // 创建调用函数路径
  const dispatchQueue: DispatchQueue = [];
  
  // 从当前 Fiber 节点出发,收集整个链路上的事件
  extractEvents();
  
  // 依次触发收集到的事件
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}
  1. 收集事件
  • fiber节点中有一个属性return,可以找到它对应的父dom所在的fiber
  • 可以从触发事件的fiber处,往上收集所有的eventListener

image.png

function extractEvents(...args): void {
  // 从当前 Fiber 节点出发,根据事件名  沿着fiber树向上收集事件listener
  const listeners = accumulateSinglePhaseListeners();

![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5e577f1c00ca4b7b9b47500675032210~tplv-k3u1fbpfcp-watermark.image?)
  if (listeners.length > 0) {
    // 创建合成事件
    const event = new SyntheticEventCtor( );
    // 加入到派发队列中
    dispatchQueue.push({ event, listeners });
  }
}
  1. 执行事件
  • 如果是冒泡, 从底层dom到顶层dom的顺序执行
  • 如果是捕获 从顶层到底层...

export function processDispatchQueue(
  dispatchQueue: DispatchQueue, // 我们前面收集的事件数组
  eventSystemFlags: EventSystemFlags, // 包含了是否是捕获阶段的信息
): void {
  // true 的话是捕获阶段,这会影响后续事件的执行顺序
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  
  for (let i = 0; i < dispatchQueue.length; i++) {
    const {event, listeners} = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);// 真实执行事件 .call
  }
}

后记

通过以上模型,想必大家也可以写出一个简易的合成事件系统。

本人也是开发了一个事件代理包,用在了自己的框架中。

想看一下具体的代码实现可以查看我的git仓库。

github.com/lzy19926/ti…