著有《React 源码》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。
文章不好写,要是有帮助别忘了点赞,收藏~ 你的鼓励是我继续挖干货的的动力🔥。
另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~
简单的介绍
有了Fiber后,要怎么构建DOM呢?
这和遍历Fiber树有关,和completeWork有关,和appendAllChildren。遍历一个Fiber节点,可以分为2个部分,开始遍历 —— beginWork,结束遍历 —— completeWork,就是在这个completeWork中,React会执行appendAllChildren创建这个Fiber的DOM,插入到DOM树上。
appendAllChildren
DOM树构建需要面对的一个问题:组件节点不是真实的DOM节点,没有DOM实例,组件里面的DOM节点要挂载到哪里?
答案是跳过组件节点挂载。
举例来说,World不会被添加到父div,而是跳过World把span添加到父div(跳过World)。
App也不会被添加到div#root,而是跳过App,把更下面的div添加到div#root。
👉Fiber 树只有HostComponent 和 HostText 会创建真实的DOM节点,DOM 树的结构由它们决定。
图片从左到右,为Fiber树,DOM树。Fiber树上,红色的表示
DOM类型Fiber节点(HostComponent HostText),黄色表示组件类型Fiber(HostRoot FunctionComponent ClassComponent HostPortal SuspenseComponent...)。
function appendAllChildren(parent, workInProgress, needsVisibilityToggle, isHidden){
let node = workInProgress.child;
while(node!==null){//---循环1
//🔴红色节点:div span p img 纯文本等
if(node.tag === HostComponent || node.tag === HostText){ //--- 插入到parent
//stateNode是dom实例
appendInitialChild(parent, node.stateNode);
}else if(node.tag===HostPortal);//啥也没做,用分号“;” ⚠️被忽略。
//🟡黄色节点: Fiber.tag = HostRoot,FunctionComponent,ClassComponent,SuspenseCompent...
else if(node.child!==null){
node.child.return = node;
node = node.child;//下钻一层,跳过黄色节点
continute;// 回到while(node!==null){}。
}
// “自身”检查 简单理解就是避免<pA><pA>xx</pA></pA>这种循环结构
if(node===workInProgress){
return;
}
//第二步:循环冒泡:当前没有兄弟节点,冒泡,更新node为父节点,
while(node.sibling===null){//---循环2
//空值检查 1.冒泡到头部(HostRoot) 2.冒泡到起点(workInProgress)
if(node.return === null||node.return === workInProgress){
return;
}
//冒泡一层,到父节点
node=node.return;
}//结束while(node.sibling===null){}
//第一步:处理完兄弟节点才能「循环冒泡」,更新node为兄弟节点
node.sibling.return = node
node = node.sibling; //回到while(node!==null){},兄弟节点继续挂载。
}//结束while(node!==null){}
}//结束appendAllChildren
function appendInitialChild(parentInstance, child) {
parentInstance.appendChild(child);
}
//简化的completeWork
function completeWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case HostComponent:{
//创建workInProgress的DOM实例。
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
}
}
}
起点是workInProgress。parent是workInProgress的DOM。node是子节点。
第一个循环可以“下钻”跳过node为组件Fiber,直到子节点是DOM Fiber。
第二个循环表示“冒泡”,如果发生了下钻,要冒泡回到组件Fiber的层级,为平移做准备。
最后两行,“平移”,切换到兄弟节点。
确保不遗漏任何子树分支。
👉FC、CC、HostRoot等本身不会生成DOM,只参与遍历。
👉HostPortal被忽略(不参与遍历,它会在其他DOM容器中继续插入)。
下钻和冒泡之间的层数是相等的,node.return === workInProgress就是回到原来的层级了。node.return === null,针对从HostRoot进入,回到HostRoot。回去后继续处理兄弟节点。
👉FC、CC、HostRoot、HostPortal的兄弟节点不会被遗漏。
每一次appendAllChildren,都是从parent出发,总是只挂载直接下级,不跨级挂载(除了FunctionComponent、ClassComponent、HostRoot)。
👉appendAllChildren 只负责 workInprogress 直接子级 DOM 的插入。
例子:
workInProgress(DOM:parent = div)
|
父tag=FunctionComponent(World) —— 3父tag=HostComponent(p) —— 4父 tag=HostComponent(p)
|下钻、冒泡 |不会发生下钻、冒泡 |不会发生下钻、冒泡
1子(span) —— 2子(span) 5子(span) —— 6子(span) 7子(span) —— 8子(span)
这一次的appendAllChildren只有1 2 3 4被挂载到parent。
5 6 7 8在之前的appendAllChildren已经被添加到3,4了,
因为遍历Fiber树,completeWork的顺序是冒泡的,是从下往上,3,4已经执行过completeWork:appendAllChildren了。
👉appendAllChildren 实际完成的是“逐子树”的 DOM 构建。
结语
React 不是一次性从Fiber 树生成整棵 DOM树。而是在遍历、构建当前Fiber节点的同时构建它的 DOM 子树。