4

一,概述

在 React 16 之前,VirtualDOM 的更新过程是采用 Stack 架构实现的,也就是循环递归方式。这种对比方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,Virtual DOM 的层级就会比较深。如果主线程被长期占用,就会阻塞渲染,造成卡顿。为了避免这种情况,需要执行更新操作时不能超过16ms,如果超过16ms,就需要先暂停,让给浏览器进行渲染操作,后续再继续执行更新计算。

而Fiber架构就是为了支持“可中断渲染”而创建的。在React中,fiber tree是一种数据结构,它可以把虚拟dom tree转换成一个链表,从而可以在执行遍历操作时支持断点重启,示意图如下。

在这里插入图片描述

二、Fiber原理

Fiber 可以理解为是一个执行单元,也可以理解为是一种数据结构。

2.1 一个执行单元

Fiber 可以理解为一个执行单元,每次执行完一个执行单元,react 就会检查现在还剩多少时间,如果没有时间则将控制权让出去。React Fiber 与浏览器的核心交互流程如下图:

在这里插入图片描述

可以看到,React 首先向浏览器请求调度,浏览器在一帧中如果还有空闲时间,会去判断是否存在待执行任务,不存在就直接将控制权交给浏览器;如果存在就会执行对应的任务,执行完成后会判断是否还有时间,有时间且有待执行任务则会继续执行下一个任务,否则将控制权交给浏览器执行渲染。

所以,我们可以将Fiber 理解为一个执行单元,并且一个执行单元必须是一次完成的,不能出现暂停,并且这个小的执行单元在认为执行完后可以移交控制权给浏览器去响应用户,从而提升渲染的效率。

2.2 一种数据结构

在官方的文档介绍中,Fiber 被解释为一种数据结构,即我们熟知的链表。每个 Virtual DOM 都可以表示为一个 fiber,如下图所示,每个节点都是一个 fiber。

在这里插入图片描述

通常,一个 fiber包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性,React Fiber 机制的实现,就是依赖于上面的数据结构。

2.3 Fiber链表结构

Fiber结构是使用的是链表,准确的说是单链表树结构,详见ReactFiber.js源码,下面我们就看下Fiber链表结构,以便后续更好的理解 Fiber 的遍历过程。

在这里插入图片描述

以上每一个单元都包含了payload(数据)和nextUpdate(指向下一个单元的指针),定义结构如下:

class Update {
  constructor(payload, nextUpdate) {
    this.payload = payload          //payload 数据
    this.nextUpdate = nextUpdate    //指向下一个节点的指针
  }
}

接下来定义一个队列,把每个单元串联起来,其中定义了两个指针:头指针firstUpdate和尾指针lastUpdate,作用是指向第一个单元和最后一个单元,并加入了baseState属性存储React中的state状态。

class UpdateQueue {
  constructor() {
    this.baseState = null // state
    this.firstUpdate = null // 第一个更新
    this.lastUpdate = null // 最后一个更新
  }
}

接下来定义两个方法:插入节点单元(enqueueUpdate)、更新队列(forceUpdate)。插入节点单元时需要考虑是否已经存在节点,如果不存在直接将firstUpdate、lastUpdate指向此节点即可。更新队列是遍历这个链表,根据payload中的内容去更新state的值

class UpdateQueue {
  //.....
  
  enqueueUpdate(update) {
    // 当前链表是空链表
    if (!this.firstUpdate) {
      this.firstUpdate = this.lastUpdate = update
    } else {
      // 当前链表不为空
      this.lastUpdate.nextUpdate = update
      this.lastUpdate = update
    }
  }
  
  // 获取state,然后遍历这个链表,进行更新
  forceUpdate() {
    let currentState = this.baseState || {}
    let currentUpdate = this.firstUpdate
    while (currentUpdate) {
      // 判断是函数还是对象,是函数则需要执行,是对象则直接返回
      let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
      currentState = { ...currentState, ...nextState }
      currentUpdate = currentUpdate.nextUpdate
    }
    // 更新完成后清空链表
    this.firstUpdate = this.lastUpdate = null
    this.baseState = currentState
    return currentState
  }
}

最后,我们写一个测试的用例:实例化一个队列,向其中加入很多节点,再更新这个队列。

let queue = new UpdateQueue()
queue.enqueueUpdate(new Update({ name: 'www' }))
queue.enqueueUpdate(new Update({ age: 10 }))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.forceUpdate()
console.log(queue.baseState);       //输出{ name:'www',age:12 }

2.4 Fiber节点

Fiber 框架的拆分单位是 fiber(fiber tree上的一个节点),实际上就是按虚拟DOM节点拆,我们需要根据虚拟dom去生成 fiber tree。 Fiber节点的数据结构如下:

{
    type: any,   //对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
    key: null | string,  //唯一标识符
    stateNode: any,  //保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
    child: Fiber | null, //大儿子
    sibling: Fiber | null, //下一个兄弟
    return: Fiber | null, //父节点
    tag: WorkTag, //定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
    nextEffect: Fiber | null, //指向下一个节点的指针
    updateQueue: mixed, //用于状态更新,回调函数,DOM更新的队列
    memoizedState: any, //用于创建输出的fiber状态
    pendingProps: any, //已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
    memoizedProps: any, //在前一次渲染期间用于创建输出的props
    // ……     
}

最终, 所有的fiber 节点通过以下属性:child,sibling 和 return来构成一个树链表。

在这里插入图片描述

其他的属性还有memoizedState(创建输出的 fiber 的状态)、pendingProps(将要改变的 props )、memoizedProps(上次渲染创建输出的 props )、pendingWorkPriority(定义 fiber 工作优先级)等等就不在过多的介绍了。

三、Fiber执行流程

Fiber的执行流程总体可以分为渲染和调度两个阶段:render阶段和commit 阶段。其中,render 阶段是可中断的,需要找出所有节点的变更;而commit 阶段是不可中断的,只会执行所有的变更。

3.1 render阶段

此阶段的主要任务就是找出所有节点的变更,如节点新增、删除、属性变更等,这些变更, React 统称为副作用,此阶段会构建一棵Fiber tree,以虚拟dom节点为维度对任务进行拆分,即一个虚拟Dom节点对应一个任务,最后产出的结果是effect list,从中统计出知道哪些节点需要更新、哪些节点需要增加、哪些节点需要删除。

3.1.1 遍历流程

React Fiber首先是将虚拟DOM树转化为Fiber tree,因此每个节点都有child、sibling、return属性,遍历Fiber tree时采用的是后序遍历方法,后序遍历的流程如下:
从顶点开始遍历;
如果有大儿子,先遍历大儿子;如果没有大儿子,则表示遍历完成;
大儿子: a. 如果有弟弟,则返回弟弟,跳到2 b. 如果没有弟弟,则返回父节点,并标志完成父节点遍历,跳到2 d. 如果没有父节点则标志遍历结束

下面是后序遍历的示意图:

在这里插入图片描述

3.1.2 收集effect list

收集effect list的具体步骤为:

1,如果当前节点需要更新,则打tag更新当前节点状态(props, state, context等);
2,为每个子节点创建fiber。如果没有产生child fiber,则结束该节点,把effect list归并到return,把此节点的sibling节点作为下一个遍历节点;否则把child节点作为下一个遍历节点;
3,如果有剩余时间,则开始下一个节点,否则等下一次主线程空闲再开始下一个节点;
4,如果没有下一个节点了,进入pendingCommit状态,此时effect list收集完毕,结束。

收集effect list的遍历顺序示意图如下:

在这里插入图片描述

3.2 commit阶段

commit 阶段需要将上阶段计算出来的需要处理的副作用一次性执行,此阶段不能暂停,否则会出现UI更新不连续的现象。此阶段需要根据effect list,将所有更新都 commit 到DOM树上。

3.2.1 根据effect list 更新视图

此阶段,根据一个 fiber 的effect list列表去更新视图,此次只列举了新增节点、删除节点、更新节点的三种操作 :

/**
* 根据一个 fiber 的 effect list 更新视图
*/
const commitWork = currentFiber => {
  if (!currentFiber) return
  let returnFiber = currentFiber.return
  let returnDOM = returnFiber.stateNode // 父节点元素
  if (currentFiber.effectTag === INSERT) {  // 如果当前fiber的effectTag标识位INSERT,则代表其是需要插入的节点
    returnDOM.appendChild(currentFiber.stateNode)
  } else if (currentFiber.effectTag === DELETE) {  // 如果当前fiber的effectTag标识位DELETE,则代表其是需要删除的节点
    returnDOM.removeChild(currentFiber.stateNode)
  } else if (currentFiber.effectTag === UPDATE) {  // 如果当前fiber的effectTag标识位UPDATE,则代表其是需要更新的节点
    if (currentFiber.type === ELEMENT_TEXT) {
      if (currentFiber.alternate.props.text !== currentFiber.props.text) {
        currentFiber.stateNode.textContent = currentFiber.props.text
      }
    }
  }
  currentFiber.effectTag = null
}

/**
* 根据一个 fiber 的 effect list 更新视图
*/
const commitRoot = () => {
  let currentFiber = workInProgressRoot.firstEffect
  while (currentFiber) {
    commitWork(currentFiber)
    currentFiber = currentFiber.nextEffect
  }
  currentRoot = workInProgressRoot // 把当前渲染成功的根fiber赋给currentRoot
  workInProgressRoot = null
}

3.2.2 完成视图更新

接下来,就是循环执行工作,当计算完成每个 fiber 的effect list后,调用 commitRoot 完成视图更新。

const workloop = (deadline) => {
  let shouldYield = false // 是否需要让出控制权
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1 // 如果执行完任务后,剩余时间小于1ms,则需要让出控制权给浏览器
  }
  if (!nextUnitOfWork && workInProgressRoot) {
    console.log('render阶段结束')
    commitRoot() // 没有下一个任务了,根据effect list结果批量更新视图
  }
  // 请求浏览器进行再次调度
  requestIdleCallback(workloop, { timeout: 1000 })
}

四、总结

相比传统的Stack架构,Fiber 将工作划分为多个工作单元,每个工作单元在执行完成后依据剩余时间决定是否让出控制权给浏览器执行渲染。 并且它设置每个工作单元的优先级,暂停、重用和中止工作单元。 每个Fiber节点都是fiber tree上的一个节点,通过子、兄弟和返回引用连接,形成一个完整的fiber tree。


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》