react为什么使用fiber链表结构去遍历组件树

947 阅读3分钟

1. 背景介绍

react fiber架构有两个主要的阶段:reconciliation/render 和 commit。在render阶段,react遍历整个组件树执行了以下操作:

  • 更新了state和props
  • 调用生命周期钩子函数
  • 遍历组件的子元素,与之前的子元素进行比较,得到需要进行的DOM更新

如果react同步的遍历整个组件树,执行上述操作,可能会执行超过16ms(如果屏幕帧率60HZ),阻塞UI渲染,造成动画掉帧,出现视觉上的卡顿等。

所以应该怎么办呢?

浏览器幕后任务协作调度 API requestIdleCallback 在浏览器空闲时间段内调用的函数排队,使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

如果我们使用一个performWork函数来执行React对组件树的整个操作,并使用requestIdleCallback API来对该任务进行调度,那么我们的代码逻辑可能是这样子:我们对一个组件进行处理并返回下一个组件等待调度,这样我们就不用像React16前的版本那样,同步的遍历整个组件树。

但是我们需要一种将render中的组件树的遍历过程分解为一个个增量单元的方法,即可以在完成某个组件的reconciliation之后,将调度权还给浏览器以执行高优先级的用户交互、ui渲染等操作,待浏览器空闲时再继续下一个组件的reconciliation。 如果继续使用之前的组件树形结构,如下图所示,我们只能用递归的方式去实现组件树的遍历。

// 深度优先遍历组件树
const root = document.getElementById('root');
function logName(node){
  console.log(node.dataset.name)
}
function traversalTree(root){
  logName(root);
  const childNodes = root.childNodes;
  for(let childNode of childNodes){
    if(childNode.nodeType !== 3){
      traversalTree(childNode);
    } 
  }
}
traversalTree(root);

//输出
a1, b1, b2, c1, d1, d2, b3, c2

递归方法非常适用于遍历树形结构,如上所示,但是递归模型无法做到增量渲染,也不能实现暂停某个组件的渲染并在浏览器空闲的时候继续执行。所以React采用了基于链表的Fiber模型

2.Fiber链表遍历过程

Fiber链表结构遍历需要以下三个字段:

  • child —— 指向第一个子节点
  • sibiling —— 指向第一个兄弟节点
  • return —— 指向父节点
// 深度优先遍历组件树
const root = document.getElementById('root');
function logName(node){
  console.log(node.dataset.name)
}
function traversalTree(root){
  logName(root);
  const childNodes = root.childNodes;
  for(let childNode of childNodes){
    if(childNode.nodeType !== 3){
      traversalTree(childNode);
    } 
  }
}
traversalTree(root);

//输出
a1, b1, b2, c1, d1, d2, b3, c2

遍历fiber链表的过程如下所示:

function workLoop(){
  while(nextUnitOfWork  && !shouldYield()){
    nextUnitOfWork = performUnitWork(nextUnitOfWork)
  }
}
const root = rootFiber;
function performUnitWork(node){
  //  这里对该节点执行render流程
        let child = perWorkOfNode(node)
    // 如果有子节点,继续遍历子节点
    if(child){
      return child;
    }
          // 如果回到了根节点,表示Fiber链表遍历完成
    if(node === root){
      return null
    }

    //如果没有子节点,也没有兄弟节点,则回父节点,如果父节点依然没有兄弟节点,则回到更上一层节点
    while(!node.sibling){
      if(!node.return || node.return === root){
        return nill
      }
      node = node.return;
    }
   
    return node.sibling;
}

上述算法使用nextUnitOfWork变量保存对当前Fiber节点的引用,能够异步的遍历组件树对应的每个Fiber节点,用requestIdleCallback包裹workLoop,使用shouldYield来检查是否有剩余时间执行nextUnitOfWork,如果没有剩余时间,则将控制权交还给浏览器,等待下一次调度从中断的nextUnitOfWork继续执行。

3.从React elements 到 Fiber Nodes

jsx->react.createElement并执行->react elements->fiber node