美文网首页Front End
[FE] React 初窥门径(九):事件的注册与分发

[FE] React 初窥门径(九):事件的注册与分发

作者: 何幻 | 来源:发表于2021-11-04 16:04 被阅读0次

    1. 背景介绍

    第六篇 文章在介绍 组件通过事件更新状态时,我们提到,

    事件绑定是在组件载入时做的

    当时只是还原了从事件触发到 onDivClick 的整个调用栈,如下所示,

    [0] dispatchDiscreteEvent [start] (user click) #6029
        [1] discreteUpdates
            [2] discreteUpdatesImpl
                [3] runWithPriority$1
                    [4] Scheduler_runWithPriority
                        [5] eventHandler=dispatchEvent
                            [6] attemptToDispatchEvent
                                [7] getClosestInstanceFromNode
                                [7] dispatchEventForPluginEventSystem
                                    [8] batchedEventUpdates
                                        [9] batchedEventUpdatesImpl=batchedEventUpdates$1
                                            [10] fn
                                                [11] dispatchEventsForPlugins
                                                    [12] processDispatchQueue
                                                        [13] processDispatchQueueItemsInOrder
                                                            [14] executeDispatch
                                                                [15] invokeGuardedCallbackAndCatchFirstError
                                                                    [16] invokeGuardedCallback
                                                                        [17] invokeGuardedCallbackImpl$1=invokeGuardedCallbackDev
                                                                            [18] fakeNode.dispatchEvent
                                                                                [19] func {onDivClick}.apply
                                                                                    [20] onDivClick [start]
    

    可以看到用户点击 div 就会直接触发 dispatchDiscreteEvent L6029

    function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
      {
        flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
      }
    
      discreteUpdates(dispatchEvent, domEventName, eventSystemFlags, container, nativeEvent);
    }
    

    然后 dispatchDiscreteEvent 又触发了一系列回调,最后调用了用户自定义的 onDivClick 事件,
    但是 dispatchDiscreteEvent 是如何绑定到 DOM 上的呢,我们之前没有介绍。

    本文我们就分析一下事件的绑定和触发整个流程。

    2. 示例项目

    github: thzt/react-tour/example-project

    组件中的事件绑定,我们是这样写的 example-project/src/AppEvent.js

    import { useState } from 'react';
    
    const App = () => {
      debugger;
      const [, setState] = useState(0);
      debugger;
    
      const onDivClick = () => {
        debugger;
        setState(1);
        debugger;
      };
    
      return <div onClick={onDivClick}>
        hello world
      </div>
    };
    
    export default App;
    

    我们给 div 上绑定了一个 onClick 事件,方法名为 onDivClick
    为了介绍事件更新的整个流程,还用了 useState 这个 hook,并在 onDivClick 中更新状态(setState)。

    我们要分析的业务流程总共分为两个部分:
    (1)组件首次加载时,事件是如何注册到 DOM 上的
    (2)用户点击 div 后,是如何触发 onDivClick

    3. 流程总览

    9.事件系统 这里我们记录了事件绑定和触发全流程,

    4. 各部分进行解释

    4.1 react-dom 库的加载过程

    第三篇 文章中我们介绍了 react-dom 库的加载过程,

    import ReactDOM from 'react-dom';
    

    会加载 react-dom.development.js 文件,文件加载过程中有一些副作用,例如生成了事件系统中要用的 allNativeEvents
    具体逻辑如下,

    [0] load [start]
        [1] (discreteEventPairsForSimpleEventPlugin=[...])
        [1] registerSimpleEvents
            [2] registerSimplePluginEventsAndSetTheirPriorities
                [3] registerTwoPhaseEvent
                    [4] registerDirectEvent
                        [5] allNativeEvents.add { 'click' }
    

    在库加载的时候,React 调用了 registerSimpleEvents 来生成静态数据。
    位于 react-dom.development.js L8338

    4.2 在 DOM 中为每个事件名 注册一个监听器

    向 DOM 注册事件 是在 ReactDOM.render 中执行的,这个过程发生在创建 Fiber Tree 之前,是在创建 FiberRootNode(Fiber Tree 根节点的 stateNode) 的时候做的,

    可参考 4.1 组件加载过程:函数组件(call stack)

    [0] render
        [1] isValidContainer
        [1] isContainerMarkedAsRoot
        [1] legacyRenderSubtreeIntoContainer
            [2] legacyCreateRootFromDOMContainer
                [3] createLegacyRoot
                    [4] new ReactDOMBlockingRoot
                        [5] createRootImpl                         <- 创建 FiberRootNode,并绑定事件
                            [6] createContainer                    <- FiberRootNode
                                [7] createFiberRoot
                                    [8] new FiberRootNode
                                    [8] createHostRootFiber
                                        [9] createFiber
                            [6] listenToAllSupportedEvents         <- 绑定事件
                                [7] listenToNativeEvent
                                    [8] addTrappedEventListener
                                        [9] addEventBubbleListener
                                            [10] addEventListener
            [2] unbatchedUpdates
                [3] fn
                    [4] updateContainer
                        [5] scheduleUpdateOnFiber
                            [6] performSyncWorkOnRoot              <- 组件首次加载
                                [7] renderRootSync
                                    [8] prepareFreshStack
                                        [9] createWorkInProgress
                                            [10] createFiber
                                    [8] markRenderStarted
                                    [8] workLoopSync               <- render 阶段
                                    [8] markRenderStopped
                                [7] commitRoot                     <- commit 阶段
    

    如果只看事件注册这一部分,就会发现 React 为一类事件绑定了同一个监听器。事件触发后,再进行分类查找具体调用哪个回调。

    [0] ReactDOM.render
        [1] render [start]
        [1] legacyRenderSubtreeIntoContainer
            [2] legacyCreateRootFromDOMContainer
                [3] createLegacyRoot
                    [4] new ReactDOMBlockingRoot
                        [5] createRootImpl
                            [6] listenToAllSupportedEvents
                                [7] allNativeEvents.forEach
                                    [8] listenToNativeEvent
                                        [9] addTrappedEventListener                           <- 从这里往下看
                                            [10] createEventListenerWrapperWithPriority
                                                [11] (listenerWrapper = dispatchDiscreteEvent) #6029
                                                    [12] (listenerWrapper.bind)
                                            [10] addEventBubbleListener
                                                [11] target.addEventListener { 'click', listener #6029, false }
            [2] unbatchedUpdates
              ...
        [1] render [end]
    

    我们从 addTrappedEventListener L8524 这里开始往下看,

    function addTrappedEventListener(...) {
    
      // 第一步:先创建一个 listener
      var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
      ...
      if (isCapturePhaseListener) {
        if (isPassiveListener !== undefined) {
          ...
        } else {
          ...
        }
      } else {
        if (isPassiveListener !== undefined) {
          ...
        } else {
    
          // 第二步:然后绑定到 DOM 上
          unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
        }
      }
    }
    

    可以看到 listener 只依赖了,

    • targetContainerdiv#root
    • domEventName'click'
    • eventSystemFlags0

    所以 click 相关的一类事件,都会统一触发这一个 listener

    我们再来看 listener 是怎么创建出来的,createEventListenerWrapperWithPriority L6007

    function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
      var eventPriority = getEventPriorityForPluginSystem(domEventName);
      ...
      switch (eventPriority) {
        case DiscreteEvent:
          listenerWrapper = dispatchDiscreteEvent;  // <- 实际的事件处理函数在这里
          break;
        ...
      }
    
      // 返回了一个 bind 函数(先传入了 3 个参数)
      return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
    }
    

    所以 click 相关的 listener 就都是 dispatchDiscreteEvent
    现在我们知道,为什么点击 div 之后,调用栈是从 dispatchDiscreteEvent 开始的了。

    [0] dispatchDiscreteEvent [start] (user click) #6029
        [1] discreteUpdates
            [2] discreteUpdatesImpl
              ...
    

    4.3 事件的分发

    当我们在页面中进行点击 click,都会触发同一个事件监听器 dispatchDiscreteEvent,然后这个事件监听器再对事件进行分发处理。

    [0] dispatchDiscreteEvent [start] (user click) #6029
        [1] discreteUpdates
            [2] discreteUpdatesImpl
                [3] runWithPriority$1
                    [4] Scheduler_runWithPriority
                        [5] eventHandler=dispatchEvent
                            [6] attemptToDispatchEvent
                                [7] getClosestInstanceFromNode { tag: 5 }           <- 获取 Fiber Node(div)【离点击位置最近的】
                                [7] dispatchEventForPluginEventSystem
                                    [8] batchedEventUpdates
                                        [9] batchedEventUpdatesImpl=batchedEventUpdates$1
                                            [10] fn
                                                [11] dispatchEventsForPlugins
                                                    [12] extractEvents$5            <- 找到绑定在 div 上面的 onDivClick 回调
                                                        [13] extractEvents$4
                                                            [14] accumulateSinglePhaseListeners
                                                                [15] getListener
                                                                    [16] getFiberCurrentPropsFromNode {children: 'hello world', onClick: onDivClick}
                                                                    [16] (listener = props[registrationName]) { onDivClick }
                                                                [15] createDispatchListener { currentTarget (div), instance (Fiber Node), listener (onDivClick) }
                                                                [15] listeners.push
                                                    [12] processDispatchQueue       <- 模拟事件冒泡,【自底向上】触发所有 Fiber Node 上的回调
                                                        [13] processDispatchQueueItemsInOrder
                                                            [14] executeDispatch
                                                                [15] invokeGuardedCallbackAndCatchFirstError
                                                                    [16] invokeGuardedCallback
                                                                        [17] invokeGuardedCallbackImpl$1=invokeGuardedCallbackDev
                                                                            [18] fakeNode.dispatchEvent
                                                                                [19] func {onDivClick}.apply
                                                                                    [20] onDivClick [start]
                                                                                    [20] setState=dispatchAction
                                                                                        [21] dispatchAction [start]
                                                                                        [21] scheduleUpdateOnFiber
                                                                                            [22] ensureRootIsScheduled
                                                                                                [23] scheduleSyncCallback
                                                                                                    [24] (syncQueue = [callback {performSyncWorkOnRoot #23142}])
                                                                                                    [24] Scheduler_scheduleCallback
                                                                                        [21] dispatchAction [end]
                                                                                    [20] onDivClick [end]
                [3] flushSyncCallbackQueue
                    [4] flushSyncCallbackQueueImpl
                        [5] runWithPriority$1
                            [6] Scheduler_runWithPriority
                                [7] eventHandler
                                    [8] callback=performSyncWorkOnRoot
                                        [9] performSyncWorkOnRoot [start] #23142
                                        [9] renderRootSync
                                        [9] commitRoot
                                        [9] performSyncWorkOnRoot [end]
        [1] dispatchDiscreteEvent [end]
    

    比较重要的步骤有以下两个,

    • getClosestInstanceFromNode:返回离鼠标点击位置,最近的那个 Fiber Node
    • processDispatchQueue:在 Fiber Tree 中自底向上进行处理,找到所有绑定了 onClick 的元素,依次触发回调

    值得注意的是,onDivClick 事件中 setState 并不会立即更新组件,而是先设置一个 syncQueue
    onDivClick 事件返回后,再由 flushSyncCallbackQueue 调用 performSyncWorkOnRoot 更新组件的。
    (详细过程,可参考 React 初窥门径(六):React 组件的更新过程

    5. 结语

    我们只看到了 React 事件系统 “冰山之一角”,React 任务是如何调度的还并不清楚,
    这需要更深入的研究学习才能看明白。


    参考

    React 初窥门径(六):React 组件的更新过程
    github: thzt/react-tour/example-project
    9.事件系统

    相关文章

      网友评论

        本文标题:[FE] React 初窥门径(九):事件的注册与分发

        本文链接:https://www.haomeiwen.com/subject/iclkzltx.html