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
只依赖了,
-
targetContainer
:div#root
-
domEventName
:'click'
-
eventSystemFlags
:0
所以 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.事件系统