React18 事件系统源码解析
前置知识
事件流包含三个阶段, 按照以下顺序依次执行
1. 事件捕获阶段
2. 处于目标阶段
3. 事件冒泡阶段
原生事件会产生一些跨平台的兼容问题
- 阻止冒泡
 
- 在微软的模型中你必须设置事件的 
cancelBubble的属性为 true - 在 W3C 模型中你必须调用事件的 
stopPropagation()方法 
function stopPropagation(event) {
  if (!event) {
    window.event.cancelBubble = true;
  }
  if (event.stopPropagation) {
    event.stopPropagation();
  }
}
- 阻止默认事件
 
- 在微软的模型中你必须设置事件的 
returnValue的属性为 false - 在 W3C 模型中你必须调用事件的 
preventDefault()方法 
function preventDefault(event) {
  if (!event) {
    window.event.returnValue = false
  }
  if (event.preventDefault) {
    event.preventDefault()
  }
}
基于以上等一些跨平台的浏览器兼容问题,React 内部实现了一套自己的合成事件。
React 事件行为
import { useRef, useEffect } from 'react'
import { createRoot } from 'react-dom/client'
function App() {
  const parentRef = useRef()
  const childRef = useRef()
  const parentBubble = () => {
    console.log('父元素React事件冒泡')
  }
  const childBubble = () => {
    console.log('子元素React事件冒泡')
  }
  const parentCapture = () => {
    console.log('父元素React事件捕获')
  }
  const childCapture = () => {
    console.log('子元素React事件捕获')
  }
  useEffect(() => {
    parentRef.current.addEventListener(
      'click',
      () => {
        console.log('父元素原生捕获')
      },
      true
    )
    parentRef.current.addEventListener('click', () => {
      console.log('父元素原生冒泡')
    })
    childRef.current.addEventListener(
      'click',
      () => {
        console.log('子元素原生捕获')
      },
      true
    )
    childRef.current.addEventListener('click', () => {
      console.log('子元素原生冒泡')
    })
    document.addEventListener(
      'click',
      () => {
        console.log('document原生捕获')
      },
      true
    )
    document.addEventListener('click', () => {
      console.log('document原生冒泡')
    })
  }, [])
  return (
    <div ref={parentRef} onClick={parentBubble} onClickCapture={parentCapture}>
      <p ref={childRef} onClick={childBubble} onClickCapture={childCapture}>
        事件执行顺序
      </p>
    </div>
  )
}
const root = createRoot(document.getElementById('root'))
root.render(<App />)
大家不看答案的情况下,如果可以说出以上代码打印的日志,那说明对 React 事件掌握的不错。
好,接下来揭晓一下答案

解析触发过程
上面代码层级如下 document -> root -> div -> p
React17-18 中是采用了事件代理的形式,将事件挂载到了 #root 上,所以。
document注册的事件最先触发。root注册的事件触发,React 根据当前点击的event.target收集对应 DOM 节点的fiber节点中的pendingProps,pendingProps在这里简单理解就是jsx中 DOM 节点对应的props,收集props中的onClickCapture(因为触发的是点击事件,所以收集onClickCapture捕获函数),最终在队列中收集成[childCapture, parentCapture],然后倒序触发。div注册的捕获事件触发。p注册的捕获事件触发。p注册的冒泡事件触发。div注册的冒泡事件触发。root收集的队列里有两个冒泡事件,[childBubble, parentBubble], 然后正序触发。document注册的冒泡事件触发。
源码部分
入口文件 ReactDOMRoot 文件中的 listenToAllSupportedEvents 方法,在 root 根 fiber 创建完之后绑定事件
1. 绑定所有简单事件
SimpleEventPlugin.registerEvents();
const listeningMarker =
  '_reactListening' +
  Math.random()
    .toString(36)
    .slice(2);
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;
    allNativeEvents.forEach(domEventName => {
      // We handle selectionchange separately because it
      // doesn't bubble and needs to be on the document.
      if (domEventName !== 'selectionchange') {
        if (!nonDelegatedEvents.has(domEventName)) {
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });
  }
}
SimpleEventPlugin.registerEvents()方法会为我们的allNativeEvents注册事件,allNativeEvents是一个Set集合。- 首先看 
#root节点上是否绑定过事件代理了,如果第一次绑定就rootContainerElement[listeningMarker] = true,防止多次代理。 allNativeEvents是所有的原生事件(内容比较多,可点击跳转源码搜索 simpleEventPluginEvents),因为有一些事件是没有冒泡行为的,比如scroll事件等 ,所以在这里根据nonDelegatedEvents区分一下是否需要绑定冒泡事件。listenToNativeEvent其实就是给我们的root事件通过addEventListener来绑定真正的事件,实现事件代理。
2. SimpleEventPlugin.registerEvents
说一下 allNativeEvents 赋值的过程
- 调用SimpleEventPlugin插件的registerEvents方法注册事件
 
SimpleEventPlugin.registerEvents();
- registerSimpleEvents
 
// registerEvents 就是 registerSimpleEvents,内部导出的时候重命名了
let topLevelEventsToReactNames = new Map()
function registerSimpleEvents() {
  for (let i = 0; i < simpleEventPluginEvents.length; i++) {
    const eventName = ((simpleEventPluginEvents[i]: any): string);
    const domEventName = ((eventName.toLowerCase(): any): DOMEventName);
    const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);
    registerSimpleEvent(domEventName, 'on' + capitalizedEvent);
  }
}
function registerSimpleEvent(domEventName: DOMEventName, reactName: string) {
  topLevelEventsToReactNames.set(domEventName, reactName);
  registerTwoPhaseEvent(reactName, [domEventName]);
}
export function registerTwoPhaseEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
): void {
  registerDirectEvent(registrationName, dependencies);
  registerDirectEvent(registrationName + 'Capture', dependencies);
}
export function registerDirectEvent(registrationName, dependencies) {
  for (let i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i])
  }
}
eventName就是click、drag、close等这些简单事件。capitalizedEvent就是React里绑定的事件了,比如上述的click、drag、close会转成onClick,onDrag,onClose。topLevelEventsToReactNames是个Map,用来建立原生事件跟React事件的映射,到时候根据触发的事件来找到 React 里映射的事件,收集fiber上的props对应的事件。registerTwoPhaseEvent方法注册捕获 + 冒泡阶段的事件。registerDirectEvent是真正的给allNativeEvents这个Set赋值。
OK,到这里我们 root 节点就已经通过事件管理绑定了所有的简单事件。
接下来就是事件触发的过程
3. 事件触发的函数
- dispatchDiscreteEvent
 
/**
 * @param {*} domEventName click 等事件名
 * @param {*} eventSystemFlags 0 冒泡  4 捕获
 * @param {*} container #root 根节点
 * @param {*} nativeEvent 原生事件 event
 */
function dispatchDiscreteEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  container: EventTarget,
  nativeEvent: AnyNativeEvent,
) {
  try {
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
  }
}
- dispatchEvent
 
export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
): void {
  dispatchEventOriginal(
      domEventName,
      eventSystemFlags,
      targetContainer,
      nativeEvent,
  );
}
- dispatchEventOriginal
 
function dispatchEventOriginal(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent,
) {
  dispatchEventForPluginEventSystem(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    null,
    targetContainer,
  );
}
- dispatchEventForPluginEventSystem
 
export function dispatchEventForPluginEventSystem(
  domEventName,
  eventSystemFlags,
  nativeEvent,
  targetInst,
  targetContainer
) {
  dispatchEventsForPlugins(
    domEventName,
    eventSystemFlags,
    nativeEvent,
    targetInst,
    targetContainer
  )
}
- dispatchEventsForPlugins
 
/**
 * 
 * @param {*} domEventName click  等原生事件
 * @param {*} eventSystemFlags 0是冒泡,4 是捕获
 * @param {*} nativeEvent 原生事件 event
 * @param {*} targetInst 当前点击 DOM 节点对应的 fiber 节点
 * @param {*} targetContainer #root
 */
function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget,
): void {
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}
export function processDispatchQueue(dispatchQueue, eventSystemFlags) {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0 //true 就是捕获
  for (let i = 0; i < dispatchQueue.length; i++) {
    const { event, listeners } = dispatchQueue[i]
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase)
  }
}
function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  if (inCapturePhase) {
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}
function executeDispatch(event, listener, currentTarget) {
  event.currentTarget = currentTarget
  listener(event)
  event.currentTarget = null
}
getEventTarget就是获取event.target,也就是我们在页面上点击的 DOM 节点。extractEvents方法会根据传过去的domEventName(比如这个事件是click),去targetInst这个 fiber 节点上去收集props里的onClick事件,fiber.return指的就是targetInst的父fiber,比如当前点击的p标签,targetInst就是p标签的 fiber,targetInst.return指的就是div的fiber节点,然后一直递归收集到dispatchQueue队列里面,最终dispatchQueue队列的数据结构就是[{event:合成事件源, listener: [{instance: p 标签的 fiber,listener:对应 p 标签的 onClick 事件,currentTarget:p 标签 DOM 节点}, div 标签的 {instance, listener, currentTarget}]}]。processDispatchQueue开始去执行我们收集的事件。processDispatchQueueItemsInOrder会判断如果当前是捕获阶段,那就倒序遍历执行我们的dispatchListeners,如果是冒泡阶段,就正序遍历执行dispatchListeners,遍历过程中还需要更改事件源上的currentTarget属性。- 整个事件阶段就完成了。
 
转载自:https://juejin.cn/post/7191308289177550906