著有《React18 设计原理》《javascript地月星》等多个专栏。欢迎关注。
创作不易,有帮助别忘了点赞,收藏,评论 ~ 你的鼓励是我继续挖干货的动力。
本文全部都是原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解 ~
推荐指数(满级):⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️
github 👉github.com/chd666233/b…
主页 👉juejin.cn/post/757791…
收集Fiber事件
事件系统的设计原理:
- 给容器绑定统一的事件监听器
- 创建合成事件对象
- ✅ 收集Fiber事件(详细、推荐阅读)
- 事件回调的派发
以一个HostPortal场景的事件收集为例:
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
index.html
<body>
<div id="rootA" class="app-container"></div>
<div id="rootB" class="app-container"></div>
</body>
Fiber树(逻辑树)
HostRoot rootA
|-- AppA
|-- div
|-- Modal
|-- HostPortal
|-- div
|-- button(点击事件源)
HostRoot rootB
|-- AppB
|-- div
|-- div div#modal-container
DOM树(真实树)
<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树。
也就是树“歪”了(Fiber树是“正”的,DOM树“歪”了)。
假如在页面上点击了button事件,事件流会是什么样的?
这就是进入我们的正文,事件的收集。
dispatchEventForPluginEventSystem
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) {
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) {
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)) {
return;
}
}
grandNode = grandNode.return;
}
}
while (container !== null) {
var parentNode = getClosestInstanceFromNode(container);
if (parentNode === null) {
return;
}
var parentTag = parentNode.tag;
if (parentTag === HostComponent || parentTag === HostText) {
//更新ancestorInst和node
node = ancestorInst = parentNode;
continue mainLoop;
}
container = container.parentNode;
}
}
node = node.return;
}
}
}
batchedUpdates(function () {
return dispatchEventsForPlugins(DOMEventName, eventSystemFlags, nativeEvent, ancestorInst);
});
}
只看代码重点:
两个回溯
-
本树回溯:本树上对container和targetContainerNode进行配对。
a.while(true){} + if(nodeTag === HostRoot✅ || nodeTag === HostPortal){}
一直冒泡到顶。
b.while(true){} + if(nodeTag === HostRoot || nodeTag === HostPortal✅){} + if (nodeTag === HostPortal✅) { while(grandNode !== null){if (✅isMatchingRootContainer(grandContainer, targetContainerNode)) {return}} }
冒泡中途遇到HostPortal,如果HostPortal的容器也在本树上,这种情况直接return退出事件收集,避免重复收集。👉详细看下文注释1。 -
跨树回溯:HostPortal的容器不在本树上,借助HostPortal的容器DOM跳转到另一颗Fiber树继续回溯。
a.while(true){} + if(nodeTag === HostRoot || nodeTag === HostPortal✅){} + if (nodeTag === HostPortal✅) { while(grandNode !== null){if (❌isMatchingRootContainer(grandContainer, targetContainerNode)) {return}} } + ✅while (container !== null) {}。
HostPortal的容器可能在本树上,也可能在其他树上。
Fiber.stateNode是什么?
1.是"容器"DOM实例,容器负责挂载内容。
<App>组件里面的节点 和 <Modal>弹窗组件里面的根节点,挂载在容器 DOM 节点上。
HostRoot Fiber.stateNode.containerInfo = div#root,
HostPortal Fiber.stateNode.containerInfo = div#modal-container。
它们的stateNode是一个{containerInfo, ...}对象。2.是具体的 DOM 实例。
HostComponent 和 HostText 的 DOM 节点
HostComponent Fiber.stateNode = div/p/span...,
HostText Fiber.stateNode = “文本内容”。
它们的 stateNode 是一个 DOM 实例。
getClosestInstanceFromNode
getClosestInstanceFromNode(node.stateNode.containerInfo)获取容器的Fiber。
function getClosestInstanceFromNode(targetNode) {//DOM类型
var targetInst = targetNode[internalInstanceKey];
if (targetInst) {
return targetInst; // Fiber 类型
}
...
}
👉 internalInstanceKey 受React管理的DOM
targetContainerNode targetInst ancestorInst
绑定事件targetContainerNode是HostRoot、HostPortal的容器。例如前面例子中的div#root和div#modal-container。
targetInst 是事件发生的DOM元素对应的Fiber实例。例如例子中的button的Fiber。
ancestorInst 是事件收集的起点。
没跨树,ancestorInst 是 button 的 Fiber;跨树了,ancestorInst 是 HostPortal 的容器(div#modal-container) 的Fiber。
isMatchingRootContainer
isMatchingRootContainer(node.stateNode.containerInfo, targetContainerNode)
当前冒泡到的容器 与 传入的容器是否同一个容器。容器匹配,就表明node在这棵Fiber树上,确定了在这棵树上,就可以更新ancestorInst。
1.在本棵树上找到匹配,则var ancestorInst = targetInst = button Fiber。
2.否则跨树了,node = ancestorInst = parentNode = div#modal-container的Fiber = getClosestInstanceFromNode(node.stateNode.containerInfo)。
function isMatchingRootContainer(grandContainer, targetContainer) {//DOM类型
return (grandContainer === targetContainer || ..);
}
回溯的流程
到目前为止,可能还不知道回溯在干嘛、更新ancestorInst在干嘛
其实观察例子就可以知道:一开始事件在button触发,原生事件在div#rootB DOM树上冒泡,直到被容器div#rootB捕获,触发React的Fiber树回溯,
targetInst是button的Fiber, 它在Fiber A树上。targetContainerNode是div#rootB。
首先在本树上回溯,就是Fiber A树,
中途遇到HostPortal,
1.从HostPortal一直冒泡到顶部,是div#rootA,HostPortal的容器不是在本树上,不用返回注释1。接着夸树收集。
2.跨树回溯,找到HostPortal的DOM容器div#modal-container,node = ancestorInst = parentNode = div#modal-container的Fiber,
continue mainLoop,就是回到while(true){},从Fiber div#modal-container向上遍历到HostRoot B。匹配。(Fiber B树回溯)。
简单的说:button DOM 被点击,事件冒泡到div#modal-container,触发事件收集函数。事件收集函数走了1、2、3、4、5👇。
//跨树部分逻辑:
//DOM树
<div id="rootA"></div>
<div id="rootB">
<div id="modal-container" 事件收集函数> 3.我是容器,找到我的Fiber
<div>
<button></button> <--点击 事件冒泡
</div>
</div>
</div>
<AppA> //Fiber A树
<HostPortal>2.中途遇到HostPortal, 找我的容器,
<div>
<button></button>1.从这个targinInst开始在Fiber A树遍历
</div>
</HostPortal>
</AppA>
<AppB> 5.匹配
<div id="modal-container"></div>4.我是容器的Fiber,现在在Fiber B树遍历
</AppB>
跨树 = FiberA -- DOM 容器 -- FiberB
HostPortal(Fiber) -- HostPortal的容器(div#modal-container DOM) -- HostPortal的容器 的Fiber(这里是第二棵树了)。
回溯添加的次数
其实这个例子一共有3个回溯: 除了冒泡到div#rootB捕获外,还有:被div#rootA捕获, 被div#modal-container捕获。
- 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个,因为DOM树是“歪”的,DOM事件不会冒泡到div#rootA,触发div#rootA上绑定的Fiber事件收集函数。
事件在div#rootB DOM树冒泡,到div#modal-container触发一次回溯,到div#rootB触发第二次回溯。
事件在DOM树上冒泡。React 给每个容器 DOM 都绑定了事件收集函数,负责回溯、收集 Fiber 树上的“事件”。
事件收集的起点和终点
回溯的作用是更新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的事件( HostRoot A的事件被收集,也不触发 div#rootA 的 DOM 事件)。
第三个 一开始ancestorInst是Fiber button,跨树回溯,更新为Fiber div#modal-container,
进入到dispatchEventsForPlugins,只收集Fiber B树从Fiber div#modal-container到HostRoot B的点击事件。
第二个:
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
注释1: 正是因为第二个会继续向上的缘故,如果HostPortal的容器没有跨树,直接返回,避免重复收集Fiber事件。想象一下,如果没有夸树,会触发div#rootA的事件收集,加上这里的HostPortal会继续向上收集,从button-HostRootA一次。button-HostPortal-HostRootA一次。就是2次。
点击关闭,先收集了A, 后收集了B :
从 ancestorInst Fiber 开始收集 Fiber“事件”,一直收集到HostPortal Fiber、HostRoot Fiber(容器Fiber)。
总结
虽然React采用的是事件委托的方式,但具体的事件、回调函数绑定在各个Fiber节点上,事件收集就是收集这些Fiber节点上的事件。
在收集前,要知道收集的Fiber起点,
容器DOM上给所有的可以委托的事件绑定了统一的事件监听器,触发dispatchEventForPluginEventSystem通过回溯的方式更新ancestorInst,作为事件收集的Fiber起点。
第一步:事件冒泡的机制,原生事件沿着DOM树冒泡,到达容器DOM。
第二步:触发绑定在容器DOM的事件,执行dispatchEventForPluginEventSystem。确定事件收集的起点,收集Fiber路径上的事件回调(可以简单的理解dispatchEventsForPlugins就是后续的事件收集,或者点击这里了解事件收集函数)。
第三步:批量执行事件回调。