1

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

1. 背景介绍

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

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

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

所以应该怎么办呢?

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

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

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

components_tree.png

// 深度优先遍历组件树
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——指向父节点

上文中的组件树对应的Fiber结构如下所示:

fiberTree.png

遍历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

我们从一个 React classComponent开始分析从React elements 到 Fiber Nodes的转化过程。

class ClickCounter extends React.Componenet{
  constructor(props) {
        super(props);
        this.state = {count: 0};
        this.handleClick = this.handleClick.bind(this);
    }

  handleClick() {
    this.setState((state) => {
      return {count: state.count + 1};
    });
  }
  render(){
    return [
      <button    key='1' onClick={this.handleClick}>Update counter</button>
            <span key='2'>{this.state.count}</span>    
    ]
  }
}

// babel jsx转化 =>

    React.createElement(
      'button',
    {
      key:'1',
      onClick: this.onClick
    },
    'Update counter'
  ),
  React.createElement(
      'span',
    {
      key:'2'
    },
    this.state.count
  )

// createElement执行得到react elements
[
   {
     $$typeof: Symbol(react.element),
     type: 'button',
     key: '1',
     props: {
       children: 'Update counter',
       onClick: () => { ... }
     }
   },
   {
     $$typeof: Symbol(react.element),
     type: 'span',
     key: "2",
     props: {
       children: 0
     }
    }
]

每个react element都有一个对应的fiber node,我们可以把fiber node看作一种描述unitWork的对象,这些对象按照链表的方式链接起来,可以方便的进行调度执行。ClickCounter组件fiber node的简略版的数据结构如下:

{
    return: null,
    child: null,
    sibling: null,
    stateNode: new ClickCounter,
    type: ClickCounter,
    tag: 1,
    alternate: null,
    key: null,
    updateQueue: null,
    memoizedProps: {},
    pendingProps: {},
    effectTag: 0,
    nextEffect: null  
}

stateNode保存类组件的实例,HostComponent的DOM节点或者其他React元素类型的类实例的引用。

type保存类组件的构造函数,或者HostComponent的Html标签,或者函数组件。

tag定义fiber的类型,例如0表示FunctionComponent,1表示ClassComponent,详情见此处

alternate指向workInProgress上的对应fiber node,current <==> workInProgress

key 是多个子组件的唯一标识,在组件更新时便于复用

updateQueue队列结构,保存状态更新、回调函数、DOM更新等

memoizedProps保存上一次渲染的props

pendingProps保存nextProps

effectTag标记该节点在commit阶段需要执行的副作用类型

nextEffect单链表用来快速查找下一个side effect

参考资料

Inside Fiber: in-depth overview of the new reconciliation algorithm in React

In-depth explanation of state and props update in React


karl
78 声望5 粉丝