🌋 React 从Fiber树中构建DOM树

421 阅读3分钟

著有《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 树的结构由它们决定。

完整的DOM树.png 图片从左到右,为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 子树。