著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。
文章不好写,要是有帮助别忘了点赞,收藏,评论 ~你的鼓励是我继续挖干货的动力🔥。
另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~
收集Fiber事件
事件系统的设计原理:
- 给容器绑定统一的事件监听器
- 创建合成事件对象
- ✅ 收集Fiber事件(详细、推荐阅读)
- 事件回调的派发
这里是第3点。以一个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退出事件收集,避免重复收集。 -
跨树回溯: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的容器可能在本树上,也可能在其他树上。
容器 负责挂载内容。
<App>组件里面的节点 和 <Modal>弹窗组件里面的根节点,挂载在容器DOM节点上。
HostRoot Fiber.stateNode.containerInfo = div#root,
HostPortal Fiber.stateNode.containerInfo = div#modal-container。
它们的stateNode是一个{containerInfo, ...}对象。
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
isMatchingRootContainer
isMatchingRootContainer(node.stateNode.containerInfo, targetContainerNode = targetContainer)当前冒泡到的容器 与 传入的容器是否同一个容器。
function isMatchingRootContainer(grandContainer, targetContainer) {//DOM类型
return (grandContainer === targetContainer || ..);
}
targetContainerNode targetInst ancestorInst
targetContainerNode是HostRoot、HostPortal的容器。例如前面例子中的div#root和div#modal-container。targetInst是事件发生的DOM元素对应的Fiber实例。例如例子中的button的Fiber。isMatchingRootContainer匹配,就表明node在这棵Fiber树上,确定了在这棵树上,就可以更新ancestorInst。
若在本棵树上找到匹配,则var ancestorInst = targetInst。
若跨树了,则node = ancestorInst = parentNode = getClosestInstanceFromNode(node.stateNode.containerInfo)。
从 ancestorInst 开始收集事件。
ancestorInst = (没跨树 button Fiber | 跨树 HostPortal的容器的Fiber)。
回溯的流程
到目前为止,可能还不知道回溯在干嘛、更新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的容器不是在本树上。
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树回溯)。
//跨树部分逻辑:
<div id="rootA"></div>
<div id="rootB">
<div id="modal-container">3.我是容器,找到我的Fiber
<div>
<button></button> <--点击
</div>
</div>
</div>
<AppA>
<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>
跨树 = HostPortal Fiber + HostPortal Fiber的容器 + HostPortal Fiber 容器的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个,因为事件是沿着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的点击事件。
第二个:
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
正是因为第二个会继续向上的缘故,如果HostPortal的容器没有跨树,直接返回,避免重复。
点击关闭,先收集了A, 后收集了B :
总结
虽然React采用的是事件委托的方式,但具体的事件、回调函数绑定在各个Fiber节点上,事件收集就是收集这些Fiber节点上的事件。
在收集前,要知道收集的Fiber起点,
容器DOM上给所有的可以委托的事件绑定了统一的事件监听器,触发dispatchEventForPluginEventSystem通过回溯的方式更新ancestorInst,作为事件收集的Fiber起点。
第一步:事件冒泡的机制,原生事件沿着DOM树冒泡,到达容器DOM。
第二步:触发绑定在容器DOM的事件,执行dispatchEventForPluginEventSystem。确定事件收集的起点,收集Fiber路径上的事件回调(可以简单的理解dispatchEventsForPlugins就是后续的事件收集,或者点击这里了解事件收集函数)。
第三步:批量执行事件回调。