一、概述

在 React 16 之前,VirtualDOM 的更新采纳的是Stack架构实现的,也就是循环递归形式。不过,这种比照形式有显著的缺点,就是一旦工作开始进行就无奈中断,如果遇到利用中组件数量比拟宏大,那么VirtualDOM 的层级就会比拟深,带来的后果就是主线程被长期占用,进而阻塞渲染、造成卡顿景象。

为了避免出现卡顿等问题,咱们必须保障在执行更新操作时计算时不能超过16ms,如果超过16ms,就须要先暂停,让给浏览器进行渲染,后续再继续执行更新计算。而Fiber架构就是为了反对“可中断渲染”而创立的。

在React中,Fiber应用了一种新的数据结构fiber tree,它能够把虚构dom tree转换成一个链表,而后再执行遍历操作,而链表在执行遍历操作时是反对断点重启的,示意图如下。

二、Fiber架构

2.1 执行单元

官网介绍中,Fiber 被了解为是一种数据结构,然而咱们也能够将它了解为是一个执行单元。

Fiber 能够了解为一个执行单元,每次执行完一个执行单元,React Fiber就会查看还剩多少工夫,如果没有工夫则将控制权让进来,而后由浏览器执行渲染操作。React Fiber 与浏览器的交互流程如下图。

能够看到,React 首先向浏览器申请调度,浏览器在执行完一帧后如果还有闲暇工夫,会去判断是否存在待执行工作,不存在就间接将控制权交给浏览器;如果存在就会执行对应的工作,执行完一个新的工作单元之后会持续判断是否还有工夫,有工夫且有待执行工作则会继续执行下一个工作,否则将控制权交给浏览器执行渲染,这个流程是循环进行的。

所以,咱们能够将Fiber 了解为一个执行单元,并且这个执行单元必须是一次实现的,不能呈现暂停。并且,这个小的执行单元在执行完后计算之后,能够移交控制权给浏览器去响应用户,从而晋升了渲染的效率。

2.2 数据结构

在官网的文档中,Fiber 被解释为是一种数据结构,即链表构造。在链表构造中,每个 Virtual DOM 都能够示意为一个 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 工作优先级)等等就不在过多的介绍了。

2.5 API

2.5.1 requestAnimationFrame

requestAnimationFrame是浏览器提供的绘制动画的 API ,它要求浏览器在下次重绘之前(即下一帧)调用指定的回调函数以更新动画。

例如,应用requestAnimationFrame实现正方形的宽度加1px,直到宽度达到100px进行,代码如下。

<body>  <div id="div" class="progress-bar "></div>  <button id="start">开始动画</button></body><script>  let btn = document.getElementById('start')  let div = document.getElementById('div')  let start = 0  let allInterval = []  const progress = () => {    div.style.width = div.offsetWidth + 1 + 'px'    div.innerHTML = (div.offsetWidth) + '%'    if (div.offsetWidth < 100) {      let current = Date.now()      allInterval.push(current - start)      start = current      requestAnimationFrame(progress)    }    }  btn.addEventListener('click', () => {    div.style.width = 0    let currrent = Date.now()    start = currrent    requestAnimationFrame(progress)  })</script>

运行下面的代码,就能够看到浏览器会在每一帧运行完结后,将div的宽度加1px,直到100px为止。

2.5.2 requestIdleCallback

requestIdleCallback 也是 Fiber 的根底 API 。requestIdleCallback能使开发者在主事件循环上执行后盾和低优先级的工作,而不会影响提早要害事件,如动画和输出响应。失常帧工作实现后没超过16ms,阐明有多余的闲暇工夫,此时就会执行requestIdleCallback里注册的工作。

具体的执行流程是,开发者采纳requestIdleCallback办法注册对应的工作,告知浏览器工作的优先级不高,如果每一帧内存在闲暇工夫,就能够执行注册的这个工作。另外,开发者是能够传入timeout参数去定义超时工夫的,如果到了超时工夫,那么浏览器必须立刻执行,应用办法如下:

window.requestIdleCallback(callback, { timeout: 1000 })。

浏览器执行完办法后,如果没有剩余时间了,或者曾经没有下一个可执行的工作了,React应该偿还控制权,并同样应用requestIdleCallback去申请下一个工夫片。具体的流程如下图:

其中,requestIdleCallback的callback中会接管到默认参数 deadline ,其中蕴含了以下两个属性:

  • timeRamining:返回以后帧还剩多少工夫供用户应用。
  • didTimeout:返回 callback 工作是否超时。

三、Fiber执行流程

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

3.1 render阶段

此阶段的次要工作就是找出所有节点产生的变更,如节点的新增、删除、属性变更等。这些变更, React 统称为副作用,此阶段会构建一棵Fiber tree,以虚构Dom节点的维度对工作进行拆分,即一个虚构Dom节点对应一个工作,最初产出的后果是副作用列表(effect list)。

3.1.1 遍历流程

在此阶段,React Fiber会将虚构DOM树转化为Fiber tree,这个Fiber tree是由节点形成的,每个节点都有child、sibling、return属性,遍历Fiber tree时采纳的是后序遍历办法,遍历的流程如下:
从顶点开始遍历;
如果有大儿子,先遍历大儿子;如果没有大儿子,则示意遍历实现;
大儿子: a. 如果有弟弟,则返回弟弟,跳到2 b. 如果没有弟弟,则返回父节点,并标记实现父节点遍历,跳到2 d. 如果没有父节点则标记遍历完结

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

此时,树结构的定义如下:

const A1 = { type: 'div', key: 'A1' }const B1 = { type: 'div', key: 'B1', return: A1 }const B2 = { type: 'div', key: 'B2', return: A1 }const C1 = { type: 'div', key: 'C1', return: B1 }const C2 = { type: 'div', key: 'C2', return: B1 }const C3 = { type: 'div', key: 'C3', return: B2 }const C4 = { type: 'div', key: 'C4', return: B2 }A1.child = B1B1.sibling = B2B1.child = C1C1.sibling = C2B2.child = C3C3.sibling = C4module.exports = A1

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收集结束,完结。

如果用代码来实现的话,首先须要遍历子虚构DOM元素数组,为每个虚构DOM元素创立子fiber。

const reconcileChildren = (currentFiber, newChildren) => {  let newChildIndex = 0  let prevSibling // 上一个子fiber  // 遍历子虚构DOM元素数组,为每个虚构DOM元素创立子fiber  while (newChildIndex < newChildren.length) {    let newChild = newChildren[newChildIndex]    let tag    // 打tag,定义 fiber类型    if (newChild.type === ELEMENT_TEXT) { // 这是文本节点      tag = TAG_TEXT    } else if (typeof newChild.type === 'string') {  // 如果type是字符串,则是原生DOM节点      tag = TAG_HOST    }    let newFiber = {      tag,      type: newChild.type,      props: newChild.props,      stateNode: null, // 还未创立DOM元素      return: currentFiber, // 父亲fiber      effectTag: INSERT, // 副作用标识,包含新增、删除、更新      nextEffect: null, // 指向下一个fiber,effect list通过nextEffect指针进行连贯    }    if (newFiber) {      if (newChildIndex === 0) {        currentFiber.child = newFiber // child为大儿子      } else {        prevSibling.sibling = newFiber // 让大儿子的sibling指向二儿子      }      prevSibling = newFiber    }    newChildIndex++  }}

该办法会收集 fiber 节点下所有的副作用,并组成effect list。每个 fiber 有两个属性:

  • firstEffect:指向第一个有副作用的子fiber。
  • lastEffect:指向最初一个有副作用的子fiber。

而咱们须要收集的就是两头nextEffect,最终造成一个单链表。

// 在实现的时候要收集有副作用的fiber,组成effect listconst completeUnitOfWork = (currentFiber) => {  // 后续遍历,儿子们实现之后,本人能力实现。最初会失去以上图中的链条构造。  let returnFiber = currentFiber.return  if (returnFiber) {    // 如果父亲fiber的firstEffect没有值,则将其指向以后fiber的firstEffect    if (!returnFiber.firstEffect) {      returnFiber.firstEffect = currentFiber.firstEffect    }    // 如果以后fiber的lastEffect有值    if (currentFiber.lastEffect) {      if (returnFiber.lastEffect) {        returnFiber.lastEffect.nextEffect = currentFiber.firstEffect      }      returnFiber.lastEffect = currentFiber.lastEffect    }    const effectTag = currentFiber.effectTag    if (effectTag) { // 阐明有副作用      // 每个fiber有两个属性:      // 1)firstEffect:指向第一个有副作用的子fiber      // 2)lastEffect:指向最初一个有副作用的子fiber      // 两头的应用nextEffect做成一个单链表      if (returnFiber.lastEffect) {        returnFiber.lastEffect.nextEffect = currentFiber      } else {        returnFiber.firstEffect = currentFiber      }      returnFiber.lastEffect = currentFiber    }  }}

最初,再定义一个递归函数,从根节点登程,把全副的 fiber 节点遍历一遍,最终产出一个effect list。

const performUnitOfWork = (currentFiber) => {  beginWork(currentFiber)  if (currentFiber.child) {    return currentFiber.child  }  while (currentFiber) {    completeUnitOfWork(currentFiber)      if (currentFiber.sibling) {        return currentFiber.sibling    }    currentFiber = currentFiber.return   }}

3.2 commit阶段

commit 阶段须要将上阶段计算出来的须要解决的副作用一次性执行,此阶段不能暂停,否则会呈现UI更新不间断的景象。此阶段须要依据effect list,将所有更新都 commit 到DOM树上。

3.2.1 依据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}

写一个递归函数,从根节点登程,依据effect list实现全副更新。

/*** 依据一个 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 })}

到此,依据收集到的变更信息实现了视图的刷新操作,Fiber的整个刷新流程也就实现了。

四、总结

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