著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。
文章不好写,要是有帮助别忘了点赞,收藏~ 你的鼓励是我继续挖干货的的动力🔥。
另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~
总流程
合成事件的设计原理:
- 给容器绑定统一的事件监听器
- 创建合成事件对象
- 收集Fiber事件 (详细)
- 事件回调的派发
这一篇处在第3点,收集Fiber事件。
树"歪"了 以一个HostPortal场景举例
jsx
function Modal(){return ReactDOM.createPortal(<div>...</div>,document.getElementById('modal-container'))}
function AppA(){return <div><Modal/></div>}
function AppB(){return <div><div id="modal-container"></div></div>}
ReactDOM.createRoot(document.getElementById('rootA')).render(<AppA />); //Root A
ReactDOM.createRoot(document.getElementById('rootB')).render(<AppB />); //Root B
html
index.html
<body>
<div id="rootA" class="app-container"></div>
<div id="rootB" class="app-container"></div>
</body>
Fiber树(逻辑树)
js
HostRoot rootA
|-- AppA
|-- div
|-- Modal
|-- HostPortal
|-- div
|-- button(点击事件源)
HostRoot rootB
|-- AppB
|-- div
|-- div div#modal-container
DOM树(真实树)
js
<body>
|-- div#rootA
| |-- <一些HTML元素>
|
|-- div#rootB
|-- div
|-- <一些HTML元素>
|-- div#modal-container
|-- div
|-- button (点击事件源)
DOM树和Fiber树是不一致的!
HostPortal内的Fiber节点,Modal、div...都在应用A的Fiber树。但是Portal渲染的DOM,都添加在应用B的DOM树。
也就是树"歪"了。

假如在页面上点击了button事件,事件流会是什么样的?
这就是进入我们的正文,事件的收集。
dispatchEventForPluginEventSystem
js
function dispatchEventForPluginEventSystem(DOMEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
var ancestorInst = targetInst;
if ((eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 && (eventSystemFlags & IS_NON_DELEGATED) === 0) {
var targetContainerNode = targetContainer;
if (targetInst !== null) {
// The below logic attempts to work out if we need to change
// the target Fiber to a different ancestor. We had similar logic
// in the legacy event system, except the big difference between
// systems is that the modern event system now has an event listener
// attached to each React Root and React Portal Root. Together,
// the DOM nodes representing these roots are the "rootContainer".
// To figure out which ancestor instance we should use, we traverse
// up the Fiber tree from the target instance and attempt to find
// root boundaries that match that of our current "rootContainer".
// If we find that "rootContainer", we find the parent Fiber
// sub-tree for that root and make that our ancestor instance.
var node = targetInst;
mainLoop: while (true) {
if (node === null) {
return;
}
var nodeTag = node.tag;
if (nodeTag === HostRoot || nodeTag === HostPortal) {
var container = node.stateNode.containerInfo;
if (isMatchingRootContainer(container, targetContainerNode)) {
break;
}
if (nodeTag === HostPortal) {
// The target is a portal, but it's not the rootContainer we're looking for.
// Normally portals handle their own events all the way down to the root.
// So we should be able to stop now. However, we don't know if this portal
// was part of *our* root.
var grandNode = node.return;
while (grandNode !== null) {
var grandTag = grandNode.tag;
if (grandTag === HostRoot || grandTag === HostPortal) {
var grandContainer = grandNode.stateNode.containerInfo;
if (isMatchingRootContainer(grandContainer, targetContainerNode)) {
// This is the rootContainer we're looking for and we found it as
// a parent of the Portal. That means we can ignore it because the
// Portal will bubble through to us.
return;
}
}
grandNode = grandNode.return;
}
}
// Now we need to find it's corresponding host Fiber in the other
// tree. To do this we can use getClosestInstanceFromNode, but we
// need to validate that the Fiber is a host instance, otherwise
// we need to traverse up through the DOM till we find the correct
// node that is from the other tree.
while (container !== null) {
var parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
var parentTag = parentNode.tag;
if (parentTag === HostComponent || parentTag === HostText) {
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
batchedUpdates(function () {
return dispatchEventsForPlugins(DOMEventName, eventSystemFlags, nativeEvent, ancestorInst);
});
}
两个回溯
-
本树回溯:本树上对container和targetContainerNode进行配对。
if(isMatchingRootContainer()){} if (nodeTag === HostPortal) {}
。 -
跨树回溯:若本树无匹配,则找到HostPortal的容器DOM。借助HostPortal的容器DOM跳转到另一颗Fiber树继续回溯。
while (container !== null) {}
。
targetContainerNode targetInst ancestorInst
targetContainerNode
是HostRoot、HostPortal的容器。例如前面例子中的div#root和div#modal-container。targetInst
是事件发生的DOM元素对应的Fiber实例。例如例子中的button的Fiber。
一旦某棵Fiber树的HostRoot HostPortal的containerInfo
和targetContainerNode
匹配,表明targetContainerNode
在这棵Fiber树上。
- 确定了在这棵树上,就可以更新
ancestorInst
若在本棵树上找到匹配,则ancestorInst=targetInst。targetInst是事件发生的DOM节点,它的Fiber。
若跨树了,则node = ancestorInst = parentNode。parentNode是getClosestInstanceFromNode(container)。
回溯的流程
到目前为止,可能还不知道回溯在干嘛、更新ancestorInst
在干嘛,其实观察例子就可以知道:一开始事件在button触发,原生事件在div#rootB DOM树上冒泡,直到被容器div#rootB捕获,触发React的Fiber树回溯,
首先在本树上回溯,就是Fiber A树,因为targetInst
是button的Fiber, 它在Fiber A树上。targetContainerNode
是div#rootB。
观察Fiber A树,就是从button向上遍历到HostPortal,从HostPortal到HostRoot A。
到顶也不能匹配,进入到第二部分的逻辑。
从button向上遍历到HostPortal,从HostPortal到HostRoot A 后没有匹配后,通过HostPortal,找到它的DOM容器div#modal-container,通过容器的Fiber向上遍历到HostRoot B,匹配。
观察Fiber A树,就是从button到HostPortal,
观察div#rootB DOM树,找到HostPortal的容器div#modal-container,
观察Fiber B树,找到容器的Fiber div#modal-container,从Fiber div#modal-container向上遍历到HostRoot B。匹配。(Fiber B树回溯)
jsx
//第二部分的逻辑:
<div id="rootA"></div>
<div id="rootB">
<div id="modal-container">3.我是容器,找到我的Fiber
<div>
<button></button> <--点击
</div>
</div>
</div>
<AppA>
<HostPortal>2.找我的容器,
<div>
<button></button>1.从这个targinInst开始在Fiber A树遍历
</div>
</HostPortal>
</AppA>
<AppB> 5.匹配
<div id="modal-container"></div>4.我是容器的Fiber,现在在Fiber B树遍历
</AppB>
回溯添加的次数
其实这个例子一共有3个回溯:
- targetInst=Fiber button, targetContainerNode='div#rootA' 不需要跨树
- targetInst=Fiber button, targetContainerNode='div#modal-container' 不需要跨树
- targetInst=Fiber button, targetContainerNode='div#rootB' 需要跨树,上面例子就是这种
为什么要这样?
想象一下,如果这个例子中不单单在HostPortal上绑定了点击事件,还在HostPortal的父节点,还在HostRoot A、B上都绑定了点击事件,这样就能收集到全部的事件。
但是这个例子的点击事件只会触发后面2个,因为事件是沿着div#rootB DOM树冒泡的,冒泡到div#modal-container触发一次回溯,冒泡到div#rootB触发第二次回溯。
事件收集的起点
回溯的作用是更新ancestorInst
,在dispatchEventsForPlugins
中Fiber树从ancestorInst
开始向上遍历收集事件。
targetContainerNode = 'div#rootA'
targetContainerNode = 'div#modal-container'
targetContainerNode = 'div#rootB'
第一个从Fiber button到HostRoot A收集一次 (不触发)
第二个ancestorInst
是Fiber button,进入到dispatchEventsForPlugins
,
收集Fiber A树,从button到HostPortal,还会继续收集从HostPortal到HostRoot A的点击事件,
没有在到达HostPortal就停下来,而是继续收集了HostPortal到HostRoot A的事件。
第三个一开始ancestorInst
是Fiber button,跨树回溯,更新为Fiber div#modal-container,
进入到dispatchEventsForPlugins
,只收集Fiber B树从Fiber div#modal-container到HostRoot B的点击事件。
js
第二个:
HostRoot rootA 继续向上 收集事件A
|-- AppA 继续向上 收集事件A
|-- div 继续向上 收集事件A
|-- Modal 继续向上 收集事件A
|-- HostPortal 收集事件A
|-- div 收集事件A
|-- button(点击事件源) 收集事件A
第三个:
HostRoot rootB 收集事件B
|-- AppB 收集事件B
|-- div 收集事件B
|-- div div#modal-container 收集事件B
点击关闭,先收集了A, 后收集了B :
![]() |
![]() |
---|
getClosestInstanceFromNode(container)
getInstanceFromNode + Closest 通过DOM Node实例获取Fiber实例,加上Closest就是 最近的祖先节点。
返回值是
- 在SSR+Suspense的场景,返回注释边界
- 没有在SSR+Suspense的场景,则直接返回自身的Fiber
对于1,这种情况,因为不是if (parentTag === HostComponent || parentTag === HostText) {}所以要继续向上,是注释的父级。
html
<div> <-- ancestorInst最终确定为这个的Fiber
<!-- $ --> <-- 返回这个
<div id="modal-container"> <-- 没有水合,没有被管理
Loading...
</div>
</!-- /$ -->
</div>
但是如果已经水合了,ancestorInst
就是Fiber div#modal-container本身。
对于2,就是我们例子的情况。container是某个HostRoot HostPortal的容器DOM(containerInfo
),它有Fiber,该Fiber可以直接返回。
例如div#modal-container是HostPortal的containerInfo
,返回div#root的Fiber。
isMatchingRootContainer
ini
function isMatchingRootContainer(grandContainer, targetContainer) {
return (
grandContainer === targetContainer || //当前container与targetContainer配对
(grandContainer.nodeType === COMMENT_NODE &&
grandContainer.parentNode === targetContainer)
);
}
条件1:当前container 与 HostRoot或HostPortal是否配对。
条件2:注释的父节点与HostRoot或HostPortal是否配对
if (nodeTag === HostRoot || nodeTag === HostPortal){}已经限制死了只能有HostRoot HostPortal,不会用到条件2。
xml
<div id="root">
<!--$-->
<div>App 内容</div>
<!--/$-->
</div>
总结
React的事件绑定在Fiber节点上,事件收集就是收集Fiber节点上的事件,批量执行。
在收集前,要知道收集的Fiber起点,
容器DOM上给所有的可以委托的事件绑定了统一的事件监听器,触发dispatchEventForPluginEventSystem
通过回溯的方式更新ancestorInst
,作为事件收集的Fiber起点。
第一步:事件冒泡的机制,原生事件沿着DOM树冒泡,到达容器DOM。
第二步:触发绑定在容器DOM的事件,执行dispatchEventForPluginEventSystem
。确定时间收集的起点,收集Fiber路径上的事件回调(可以简单的理解dispatchEventsForPlugins
就是后续的事件收集)。
第三步:批量执行事件回调。