• 【React源码】(十六)React 合成事件


    React 合成事件

    概览

    v17.0.0开始, React 不会再将事件处理添加到 document 上, 而是将事件处理添加到渲染 React 树的根 DOM 容器中.

    引入官方提供的图片:

    图中清晰的展示了v17.0.0的改动, 无论是在document还是根 DOM 容器监听事件, 都可以归为事件委托(代理)(mdn).

    注意: react的事件体系, 不是全部都通过事件委托来实现的. 有一些特殊情况, 是直接绑定到对应 DOM 元素上的(如:scrollload), 它们都通过listenToNonDelegatedEvent函数进行绑定.

    上述特殊事件最大的不同是监听的 DOM 元素不同, 除此之外, 其他地方的实现与正常事件大体一致.

    本节讨论的是可以被根 DOM 容器代理的正常事件.

    事件绑定

    在前文React 应用的启动过程中介绍了React在启动时会创建全局对象, 其中在创建fiberRoot对象时, 调用createRootImpl:

     
    
    1. function createRootImpl(
    2. container: Container,
    3. tag: RootTag,
    4. options: void | RootOptions,
    5. ) {
    6. // ... 省略无关代码
    7. if (enableEagerRootListeners) {
    8. const rootContainerElement =
    9. container.nodeType === COMMENT_NODE ? container.parentNode : container;
    10. listenToAllSupportedEvents(rootContainerElement);
    11. }
    12. // ... 省略无关代码
    13. }

    listenToAllSupportedEvents函数, 实际上完成了事件代理:

     
    
    1. // ... 省略无关代码
    2. export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
    3. if (enableEagerRootListeners) {
    4. // 1. 节流优化, 保证全局注册只被调用一次
    5. if ((rootContainerElement: any)[listeningMarker]) {
    6. return;
    7. }
    8. (rootContainerElement: any)[listeningMarker] = true;
    9. // 2. 遍历allNativeEvents 监听冒泡和捕获阶段的事件
    10. allNativeEvents.forEach(domEventName => {
    11. if (!nonDelegatedEvents.has(domEventName)) {
    12. listenToNativeEvent(
    13. domEventName,
    14. false, // 冒泡阶段监听
    15. ((rootContainerElement: any): Element),
    16. null,
    17. );
    18. }
    19. listenToNativeEvent(
    20. domEventName,
    21. true, // 捕获阶段监听
    22. ((rootContainerElement: any): Element),
    23. null,
    24. );
    25. });
    26. }
    27. }

    核心逻辑:

    1. 节流优化, 保证全局注册只被调用一次.
    2. 遍历allNativeEvents, 调用listenToNativeEvent监听冒泡和捕获阶段的事件.
      • allNativeEvents包括了大量的原生事件名称, 它是在DOMPluginEventSystem.js被初始化

    listenToNativeEvent:

     
    
    1. // ... 省略无关代码
    2. export function listenToNativeEvent(
    3. domEventName: DOMEventName,
    4. isCapturePhaseListener: boolean,
    5. rootContainerElement: EventTarget,
    6. targetElement: Element | null,
    7. eventSystemFlags?: EventSystemFlags = 0,
    8. ): void {
    9. let target = rootContainerElement;
    10. const listenerSet = getEventListenerSet(target);
    11. const listenerSetKey = getListenerSetKey(
    12. domEventName,
    13. isCapturePhaseListener,
    14. );
    15. // 利用set数据结构, 保证相同的事件类型只会被注册一次.
    16. if (!listenerSet.has(listenerSetKey)) {
    17. if (isCapturePhaseListener) {
    18. eventSystemFlags |= IS_CAPTURE_PHASE;
    19. }
    20. // 注册事件监听
    21. addTrappedEventListener(
    22. target,
    23. domEventName,
    24. eventSystemFlags,
    25. isCapturePhaseListener,
    26. );
    27. listenerSet.add(listenerSetKey);
    28. }
    29. }

    addTrappedEventListener:

     
    
    1. // ... 省略无关代码
    2. function addTrappedEventListener(
    3. targetContainer: EventTarget,
    4. domEventName: DOMEventName,
    5. eventSystemFlags: EventSystemFlags,
    6. isCapturePhaseListener: boolean,
    7. isDeferredListenerForLegacyFBSupport?: boolean,
    8. ) {
    9. // 1. 构造listener
    10. let listener = createEventListenerWrapperWithPriority(
    11. targetContainer,
    12. domEventName,
    13. eventSystemFlags,
    14. );
    15. let unsubscribeListener;
    16. // 2. 注册事件监听
    17. if (isCapturePhaseListener) {
    18. unsubscribeListener = addEventCaptureListener(
    19. targetContainer,
    20. domEventName,
    21. listener,
    22. );
    23. } else {
    24. unsubscribeListener = addEventBubbleListener(
    25. targetContainer,
    26. domEventName,
    27. listener,
    28. );
    29. }
    30. }
    31. // 注册原生事件 冒泡
    32. export function addEventBubbleListener(
    33. target: EventTarget,
    34. eventType: string,
    35. listener: Function,
    36. ): Function {
    37. target.addEventListener(eventType, listener, false);
    38. return listener;
    39. }
    40. // 注册原生事件 捕获
    41. export function addEventCaptureListener(
    42. target: EventTarget,
    43. eventType: string,
    44. listener: Function,
    45. ): Function {
    46. target.addEventListener(eventType, listener, true);
    47. return listener;
    48. }

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

    原生 listener

    在注册原生事件的过程中, 需要重点关注一下监听函数, 即listener函数. 它实现了把原生事件派发到react体系之内, 非常关键.

    比如点击 DOM 触发原生事件, 原生事件最后会被派发到react内部的onClick函数. listener函数就是这个由外至内的关键环节.

    listener是通过createEventListenerWrapperWithPriority函数产生:

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

    可以看到, 不同的domEventName调用getEventPriorityForPluginSystem后返回不同的优先级, 最终会有 3 种情况:

    1. DiscreteEvent: 优先级最高, 包括click, keyDown, input等事件, 源码
    2. UserBlockingEvent: 优先级适中, 包括drag, scroll等事件, 源码
    3. ContinuousEvent: 优先级最低,包括animation, load等事件, 源码

    这 3 种listener实际上都是对dispatchEvent的包装:

     
    
    1. // ...省略无关代码
    2. export function dispatchEvent(
    3. domEventName: DOMEventName,
    4. eventSystemFlags: EventSystemFlags,
    5. targetContainer: EventTarget,
    6. nativeEvent: AnyNativeEvent,
    7. ): void {
    8. if (!_enabled) {
    9. return;
    10. }
    11. const blockedOn = attemptToDispatchEvent(
    12. domEventName,
    13. eventSystemFlags,
    14. targetContainer,
    15. nativeEvent,
    16. );
    17. }

    事件触发

    当原生事件触发之后, 首先会进入到dispatchEvent这个回调函数. 而dispatchEvent函数是react事件体系中最关键的函数, 其调用链路较长, 核心步骤如图所示:

    重点关注其中 3 个核心环节:

    1. attemptToDispatchEvent
    2. SimpleEventPlugin.extractEvents
    3. processDispatchQueue

    关联 fiber

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

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

    核心逻辑:

    1. 定位原生 DOM 节点: 调用getEventTarget
    2. 获取与 DOM 节点对应的 fiber 节点: 调用getClosestInstanceFromNode
    3. 通过插件系统, 派发事件: 调用 dispatchEventForPluginEventSystem

    收集 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. }

    核心逻辑:

    1. 收集所有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. }

    2. 构造合成事件(SyntheticEvent), 添加到派发队列(dispatchQueue)

    构造合成事件

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

    具体的构造过程并不复杂, 可以直接查看源码.

    此处我们需要知道, 在Plugin.extractEvents过程中, 遍历fiber树找到listener之后, 就会创建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. }
    12. function processDispatchQueueItemsInOrder(
    13. event: ReactSyntheticEvent,
    14. dispatchListeners: Array,
    15. inCapturePhase: boolean,
    16. ): void {
    17. let previousInstance;
    18. if (inCapturePhase) {
    19. // 1. capture事件: 倒序遍历listeners
    20. for (let i = dispatchListeners.length - 1; i >= 0; i--) {
    21. const { instance, currentTarget, listener } = dispatchListeners[i];
    22. if (instance !== previousInstance && event.isPropagationStopped()) {
    23. return;
    24. }
    25. executeDispatch(event, listener, currentTarget);
    26. previousInstance = instance;
    27. }
    28. } else {
    29. // 2. bubble事件: 顺序遍历listeners
    30. for (let i = 0; i < dispatchListeners.length; i++) {
    31. const { instance, currentTarget, listener } = dispatchListeners[i];
    32. if (instance !== previousInstance && event.isPropagationStopped()) {
    33. return;
    34. }
    35. executeDispatch(event, listener, currentTarget);
    36. previousInstance = instance;
    37. }
    38. }
    39. }

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

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

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

    总结

    从架构上来讲, SyntheticEvent打通了从外部原生事件到内部fiber树的交互渠道, 使得react能够感知到浏览器提供的原生事件, 进而做出不同的响应, 修改fiber树, 变更视图等.

    从实现上讲, 主要分为 3 步:

    1. 监听原生事件: 对齐DOM元素fiber元素
    2. 收集listeners: 遍历fiber树, 收集所有监听本事件的listener函数.
    3. 派发合成事件: 构造合成事件, 遍历listeners进行派发.

  • 相关阅读:
    【漏洞复现】金和OA FileUploadMessage 文件读取
    中后台管理系统如何更优雅的支持移动端
    单体 or 微服务?你以为是架构权衡?其实是认知负载!
    【深度学习】 Python 和 NumPy 系列教程(八):Python类
    JAVA并发编程——CAS与AQS源码详解
    Prometheus服务发现之kubernetes_sd_config
    【网络】总览(待更新)
    为什么要使用双重校验锁来实现单例模式?
    基于Electron24+Vite4+Vue3搭建桌面端应用
    什么是类加载器,类加载器如何分类
  • 原文地址:https://blog.csdn.net/weixin_44828588/article/details/126546223