在学习React的合成事件之前,重新提一下浏览器的事件系统和事件委托,这对理解React事件系统源码非常重要。

事件触发的顺序是从下至上,同一个元素上的事件按照绑定的顺序执行。

React16.X版本中,并不是将事件绑在该div的真实DOM上,而是在document处监听所有支持的事件,当 DOM 事件触发后冒泡到document,React 找到对应的组件,造一个 React 事件(SyntheticEvent)出来,并按组件树模拟一遍事件冒泡(此时原生 DOM 事件早已冒出document了)

从v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中,在这里我们主要看17.02版本的合成事件

为什么要实现合成事件

SyntheticEvent:是react内部创建的一个对象,是原生事件的跨浏览器包装器, 拥有和浏览器原生事件相同的接口(stopPropagation,preventDefault), 抹平不同浏览器 api 的差异,兼容性好。

1、解决了跨浏览器的兼容性问题

2、避免这类DOM事件滥用,如果DOM上绑定了过多的事件处理函数,整个页面响应以及内存占用可能都会受到影响

合成事件的原理是什么?

  • React 基于 Virtual DOM 实现了一个SyntheticEvent(合成事件)层,我们所定义的事件处理器会接收到一个SyntheticEvent对象的实例,同样支持事件的冒泡机制,我们可以使用stopPropagation()和preventDefault()来中断它。
  • 所有事件都自动绑定到最外层上(document)

React 事件机制 - 图1

具体到代码:ReactFiberCompleteWork.old.js

React事件注册

事件注册是自执行的,也就是React自身进行调用的,过程如下:

DOMpluginEventSystem.js
👇
registerSimpleEvents
👇
registerTwoPhaseEvent
👇
registerDirectEvent
👇
allNativeEvents

React事件就是在组件中调用的onClick这种写法的事件。DOMpluginEventSystem.js中分为5个函数写,主要是区分不同的事件注册逻辑,但是最后都会添加到allNativeEventsSet数据结构中。

React事件绑定

创建根Fiber节点之后,调用listenToAllSupportedEvents方法,其中rootContainerElement参数就是应用中的id = root的DOM元素。

1.listenToAllSupportedEvents

这个函数主要遍历allNativeEvents, 调用listenToNativeEvent监听冒泡和捕获阶段的事件

  1. export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  2. if (enableEagerRootListeners) {
  3. (rootContainerElement: any)[listeningMarker] = true;
  4. allNativeEvents.forEach(domEventName => {
  5. if (!nonDelegatedEvents.has(domEventName)) {
  6. listenToNativeEvent(
  7. domEventName,
  8. false,
  9. ((rootContainerElement: any): Element),
  10. null,
  11. );
  12. }
  13. listenToNativeEvent(
  14. domEventName,
  15. true,
  16. ((rootContainerElement: any): Element),
  17. null,
  18. );
  19. });
  20. }
  21. }

2.listenToNativeEvent

  1. export function listenToNativeEvent(
  2. domEventName: DOMEventName,
  3. isCapturePhaseListener: boolean,
  4. rootContainerElement: EventTarget,
  5. targetElement: Element | null,
  6. eventSystemFlags?: EventSystemFlags = 0,
  7. ): void {
  8. let target = rootContainerElement;
  9. if (
  10. domEventName === 'selectionchange' &&
  11. (rootContainerElement: any).nodeType !== DOCUMENT_NODE
  12. ) {
  13. target = (rootContainerElement: any).ownerDocument;
  14. }
  15. if (
  16. targetElement !== null &&
  17. !isCapturePhaseListener &&
  18. nonDelegatedEvents.has(domEventName)
  19. ) {
  20. if (domEventName !== 'scroll') {
  21. return;
  22. }
  23. eventSystemFlags |= IS_NON_DELEGATED;
  24. target = targetElement;
  25. }
  26. const listenerSet = getEventListenerSet(target);
  27. const listenerSetKey = getListenerSetKey(
  28. domEventName,
  29. isCapturePhaseListener,
  30. );
  31. // If the listener entry is empty or we should upgrade, then
  32. // we need to trap an event listener onto the target.
  33. if (!listenerSet.has(listenerSetKey)) {
  34. if (isCapturePhaseListener) {
  35. eventSystemFlags |= IS_CAPTURE_PHASE;
  36. }
  37. addTrappedEventListener(
  38. target,
  39. domEventName,
  40. eventSystemFlags,
  41. isCapturePhaseListener,
  42. );
  43. listenerSet.add(listenerSetKey);
  44. }
  45. }

3.addTrappedEventListener

创建具有优先级的监听函数

  1. function addTrappedEventListener(
  2. targetContainer: EventTarget,
  3. domEventName: DOMEventName,
  4. eventSystemFlags: EventSystemFlags,
  5. isCapturePhaseListener: boolean,
  6. isDeferredListenerForLegacyFBSupport?: boolean,
  7. ) {
  8. let listener = createEventListenerWrapperWithPriority(
  9. targetContainer,
  10. domEventName,
  11. eventSystemFlags,
  12. );
  13. let isPassiveListener = undefined;
  14. if (passiveBrowserEventsSupported) {
  15. if (
  16. domEventName === 'touchstart' ||
  17. domEventName === 'touchmove' ||
  18. domEventName === 'wheel'
  19. ) {
  20. isPassiveListener = true;
  21. }
  22. }
  23. targetContainer =
  24. enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
  25. ? (targetContainer: any).ownerDocument
  26. : targetContainer;
  27. let unsubscribeListener;
  28. if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
  29. const originalListener = listener;
  30. listener = function(...p) {
  31. removeEventListener(
  32. targetContainer,
  33. domEventName,
  34. unsubscribeListener,
  35. isCapturePhaseListener,
  36. );
  37. return originalListener.apply(this, p);
  38. };
  39. }
  40. // TODO: There are too many combinations here. Consolidate them.
  41. if (isCapturePhaseListener) {
  42. if (isPassiveListener !== undefined) {
  43. unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
  44. targetContainer,
  45. domEventName,
  46. listener,
  47. isPassiveListener,
  48. );
  49. } else {
  50. unsubscribeListener = addEventCaptureListener(
  51. targetContainer,
  52. domEventName,
  53. listener,
  54. );
  55. }
  56. } else {
  57. if (isPassiveListener !== undefined) {
  58. unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
  59. targetContainer,
  60. domEventName,
  61. listener,
  62. isPassiveListener,
  63. );
  64. } else {
  65. unsubscribeListener = addEventBubbleListener(
  66. targetContainer,
  67. domEventName,
  68. listener,
  69. );
  70. }
  71. }
  72. }

listenToAllSupportedEvents开始, 调用链路比较长,最后调用addEventBubbleListeneraddEventCaptureListener监听了原生事件。

4.createEventListenerWrapperWithPriority

在注册原生事件的过程中,需要重点关注一下监听函数, 即listener函数. 它实现了把原生事件派发到react体系之内, 非常关键:比如点击 DOM 触发原生事件, 原生事件最后会被派发到react内部的onClick函数。listener函数就是这个由外至内的关键环节。

和事件注册一样,listener也分为dispatchDiscreteEvent, dispatchUserBlockingUpdate, dispatchEvent三种。它们之间的主要区别是执行优先级,还有discreteEvent涉及到要清除之前的discreteEvent问题,所以做了区分。但是它们最后都会调用dispatchEvent,最后绑定到div#root 上的这个统一的事件分发函数,其实就是 dispatchEvent。

  1. export function createEventListenerWrapperWithPriority(
  2. targetContainer: EventTarget,
  3. domEventName: DOMEventName,
  4. eventSystemFlags: EventSystemFlags,
  5. ): Function {
  6. const eventPriority = getEventPriorityForPluginSystem(domEventName);
  7. let listenerWrapper;
  8. switch (eventPriority) {
  9. case DiscreteEvent:
  10. listenerWrapper = dispatchDiscreteEvent;
  11. break;
  12. case UserBlockingEvent:
  13. listenerWrapper = dispatchUserBlockingUpdate;
  14. break;
  15. case ContinuousEvent:
  16. default:
  17. listenerWrapper = dispatchEvent;
  18. break;
  19. }
  20. return listenerWrapper.bind(
  21. null,
  22. domEventName,
  23. eventSystemFlags,
  24. targetContainer,
  25. );
  26. }

React事件触发

当原生事件触发之后,首先会进入到dispatchEvent这个回调函数。
React 事件机制 - 图2

attemptToDispatchEvent把原生事件和fiber树关联起来。

核心逻辑:

  1. 定位原生 DOM 节点: 调用getEventTarget
  2. 获取与 DOM 节点对应的 fiber 节点: 调用getClosestInstanceFromNode
  3. 通过插件系统, 派发事件: 调用 dispatchEventForPluginEventSystem
  1. export function attemptToDispatchEvent(
  2. domEventName: DOMEventName,
  3. eventSystemFlags: EventSystemFlags,
  4. targetContainer: EventTarget,
  5. nativeEvent: AnyNativeEvent,
  6. ): null | Container | SuspenseInstance {
  7. // 1. 定位原生DOM节点
  8. const nativeEventTarget = getEventTarget(nativeEvent);
  9. // 2. 获取与DOM节点对应的fiber节点
  10. let targetInst = getClosestInstanceFromNode(nativeEventTarget);
  11. ......
  12. ......
  13. ......
  14. // 3. 通过插件系统,派发事件
  15. dispatchEventForPluginEventSystem(
  16. domEventName,
  17. eventSystemFlags,
  18. nativeEvent,
  19. targetInst,
  20. targetContainer,
  21. );
  22. // We're not blocked on anything.
  23. return null;
  24. }

然后进行收集 fiber 上的 listener:

dispatchEvent函数的调用过程中, 通过不同的插件,处理不同的事件。其中最常见的事件都会由SimpleEventPlugin.extractEvents进行处理:

  1. function extractEvents(
  2. dispatchQueue: DispatchQueue,
  3. domEventName: DOMEventName,
  4. targetInst: null | Fiber,
  5. nativeEvent: AnyNativeEvent,
  6. nativeEventTarget: null | EventTarget,
  7. eventSystemFlags: EventSystemFlags,
  8. targetContainer: EventTarget,
  9. ): void {
  10. const reactName = topLevelEventsToReactNames.get(domEventName);
  11. if (reactName === undefined) {
  12. return;
  13. }
  14. let SyntheticEventCtor = SyntheticEvent;
  15. let reactEventType: string = domEventName;
  16. const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  17. const accumulateTargetOnly = !inCapturePhase && domEventName === 'scroll';
  18. // 1. 收集所有监听该事件的函数.
  19. const listeners = accumulateSinglePhaseListeners(
  20. targetInst,
  21. reactName,
  22. nativeEvent.type,
  23. inCapturePhase,
  24. accumulateTargetOnly,
  25. );
  26. if (listeners.length > 0) {
  27. // 2. 构造合成事件, 添加到派发队列
  28. const event = new SyntheticEventCtor(
  29. reactName,
  30. reactEventType,
  31. null,
  32. nativeEvent,
  33. nativeEventTarget,
  34. );
  35. dispatchQueue.push({ event, listeners });
  36. }
  37. }

收集所有listener回调,这里收集的是fiber.memoizedProps.onClick/onClickCapture等绑定在fiber节点上的回调函数

具体逻辑在accumulateSinglePhaseListeners:

  1. export function accumulateSinglePhaseListeners(
  2. targetFiber: Fiber | null,
  3. reactName: string | null,
  4. nativeEventType: string,
  5. inCapturePhase: boolean,
  6. accumulateTargetOnly: boolean,
  7. ): Array<DispatchListener> {
  8. const captureName = reactName !== null ? reactName + 'Capture' : null;
  9. const reactEventName = inCapturePhase ? captureName : reactName;
  10. const listeners: Array<DispatchListener> = [];
  11. let instance = targetFiber;
  12. let lastHostComponent = null;
  13. // 从targetFiber开始, 向上遍历, 直到 root 为止
  14. while (instance !== null) {
  15. const { stateNode, tag } = instance;
  16. // 当节点类型是HostComponent时(如: div, span, button等类型)
  17. if (tag === HostComponent && stateNode !== null) {
  18. lastHostComponent = stateNode;
  19. if (reactEventName !== null) {
  20. // 获取标准的监听函数 (如onClick , onClickCapture等)
  21. const listener = getListener(instance, reactEventName);
  22. if (listener != null) {
  23. listeners.push(
  24. createDispatchListener(instance, listener, lastHostComponent),
  25. );
  26. }
  27. }
  28. }
  29. // 如果只收集目标节点, 则不用向上遍历, 直接退出
  30. if (accumulateTargetOnly) {
  31. break;
  32. }
  33. instance = instance.return;
  34. }
  35. return listeners;
  36. }

收集完之后,进行构造合成事件(SyntheticEvent)添加到派发队列(dispatchQueue)

extractEvents完成之后, 在processDispatchQueue方法中执行真正的派发工作

  1. export function processDispatchQueue(
  2. dispatchQueue: DispatchQueue,
  3. eventSystemFlags: EventSystemFlags,
  4. ): void {
  5. const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  6. for (let i = 0; i < dispatchQueue.length; i++) {
  7. const { event, listeners } = dispatchQueue[i];
  8. processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  9. }
  10. // ...
  11. }

其中调用的processDispatchQueueItemsInOrder方法:

  1. function processDispatchQueueItemsInOrder(
  2. event: ReactSyntheticEvent,
  3. dispatchListeners: Array<DispatchListener>,
  4. inCapturePhase: boolean,
  5. ): void {
  6. let previousInstance;
  7. if (inCapturePhase) {
  8. // 1. capture事件: 倒序遍历listeners
  9. for (let i = dispatchListeners.length - 1; i >= 0; i--) {
  10. const { instance, currentTarget, listener } = dispatchListeners[i];
  11. if (instance !== previousInstance && event.isPropagationStopped()) {
  12. return;
  13. }
  14. executeDispatch(event, listener, currentTarget);
  15. previousInstance = instance;
  16. }
  17. } else {
  18. // 2. bubble事件: 顺序遍历listeners
  19. for (let i = 0; i < dispatchListeners.length; i++) {
  20. const { instance, currentTarget, listener } = dispatchListeners[i];
  21. if (instance !== previousInstance && event.isPropagationStopped()) {
  22. return;
  23. }
  24. executeDispatch(event, listener, currentTarget);
  25. previousInstance = instance;
  26. }
  27. }
  28. }

processDispatchQueueItemsInOrder函数中遍历dispatchListeners数组,执行executeDispatch派发事件,在fiber节点上绑定的listener函数被执行。

processDispatchQueueItemsInOrder函数中,根据捕获(captuer)或冒泡(bubble)的不同,采取了不同的遍历方式:

  • capture事件: 从上至下调用fiber树中绑定的回调函数,所以倒序遍历dispatchListeners.。
  • bubble事件: 从下至上调用fiber树中绑定的回调函数,所以顺序遍历dispatchListeners。

例子理解

点击codesandbox查看代码

  1. export default class Test1 extends React.Component {
  2. innerClick = () => {
  3. console.log('A: react inner click.')
  4. }
  5. outerClick = () => {
  6. console.log('B: react outer click.')
  7. }
  8. componentDidMount() {
  9. document.getElementById('outer').addEventListener('click', () => {
  10. console.log('C: native outer click')
  11. })
  12. document.getElementById('inner').addEventListener('click', () => {
  13. console.log('D: native inner click')
  14. })
  15. }
  16. render() {
  17. return (
  18. <div id='outer' onClick={this.outerClick}>
  19. <button id='inner' onClick={this.innerClick}>
  20. BUTTON
  21. </button>
  22. </div>
  23. )
  24. }
  25. }

在root上监听了 A:inner、B:outer
在div上监听了 C:outer
在button上监听了 D:inner
当点击button,从触发的节点冒泡,一次为DCAB

  1. export default class Test2 extends React.Component {
  2. innerClick = (e) => {
  3. console.log('A: react inner click.')
  4. e.stopPropagation()
  5. }
  6. outerClick = () => {
  7. console.log('B: react outer click.')
  8. }
  9. componentDidMount() {
  10. document.getElementById('outer').addEventListener('click', () => {
  11. console.log('C: native outer click')
  12. })
  13. document.getElementById('inner').addEventListener('click', () => {
  14. console.log('D: native inner click')
  15. })
  16. }
  17. render() {
  18. return (
  19. <div id='outer' onClick={this.outerClick}>
  20. <button id='inner' onClick={this.innerClick}>
  21. BUTTON
  22. </button>
  23. </div>
  24. )
  25. }
  26. }

在root上监听了 A:inner、B:outer
在div上监听了 C:outer
在button上监听了 D:inner
当点击button,从触发的节点冒泡,依次为DCA,由于调用了合成事件的方法stopPropagation,会阻止B的回调。

  1. export default class Test4 extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. }
  5. innerClick = (e) => {
  6. e.nativeEvent.stopImmediatePropagation()
  7. console.log("A: react inner click.");
  8. };
  9. outerClick = () => {
  10. console.log("B: react outer click.");
  11. };
  12. componentDidMount() {
  13. document.getElementById("root").addEventListener("click", () => {
  14. console.log("D: native document click");
  15. });
  16. }
  17. render() {
  18. return (
  19. <div id="outer" onClick={this.outerClick}>
  20. <button id="inner" onClick={this.innerClick}>
  21. BUTTON
  22. </button>
  23. </div>
  24. );
  25. }
  26. }

在root上监听了 A:inner、B:outer、D:native
点击BUTTON依次执行AB,冒泡依次执行的时候A中调用了nativeEvent.stopImmediatePropagation,会阻止事件在本元素中继续扩散,也就是会阻止D的原生事件。

这里为什么D在root的最后执行,原因是componentDidMount的时候才收集到这个函数。

总结

  • 在React代码执行时,内部会自动执行事件的注册;
  • 事件监听:第一次渲染,创建fiberRoot时,会进行原生事件的监听,所有的事件通过addEventListener委托在id=rootDOM元素上进行监听,并且对齐DOM元素fiber元素
  • 收集listeners: 遍历fiber树,收集所有监听本事件的listener函数。
  • 派发合成事件: 触发事件时,会进行事件合成,同类型事件复用一个合成事件类实例对象,遍历listeners进行派发,执行代码中的回调函数。