React-fiber

朱欣欣
起源

  React的核心概念即数据到UI的映射是通过纯函数(UI = f(DATA))实现的,所以无论何时,只要DATA一致,UI必定相同。

  但往往页面UI是非常复杂的,对于通常的app来说,无法通过一个纯函数实现,所以有了组件,即Component。Component隐藏了组件内部的实现,对外只暴露出一个类似functional的接口,如一个ViewList可能只暴露出一个data属性,。而且这种函数式的组件调用往往存在很深的嵌套。这时我们就可以想象每个Component的渲染就像是一个个函数在(嵌套)调用,那么自然而然就有了调用栈。

  虽然react在调用栈上做了足够多的优化,比如函数结果缓存策略等,但调用栈的深度和复杂度随着UI元素的增加使得页面越来越卡,但往往这些函数调用过程中,许多开销不是我们想要的:

  • 不是每一次状态的更新都要立刻执行并渲染给用户看。
  • 不同的状态更新应该有不同的优先级定义

fiber之前react都是直接通过setState立即执行更新,通过原生调用栈实现,这导致我们无法去控制内部的调用顺序、暂停等,无法干扰其执行。所以大量的函数栈调用会在react的重绘中执行使得UI变卡。

设计思想

  在当前版本的实现中,React在一个工作周期中会递归地遍历要更新的树并且反复调用render方法,而fiber的出现则实现了函数的虚拟调用栈,类似于虚拟dom层一样,在setState触发的状态更新导致函数调用的过程中加入了一层虚拟stack层,每次状态更新都会进入虚拟stack层,然后再一次性提交给真正的stack去执行,这样react就能在虚拟stack层中据某些规则进行任务的调度,从而使某些任务被延迟执行,优化了提交给无法干扰其执行的原生stack去执行。在虚拟stack层中,可以实现:

  • 暂停、恢复任务。
  • 不同的任务拥有不同的优先级定义(比如过渡动画应该比数据更新拥有更高的优先级)。
  • 复用已完成的任务。
  • 中止任务。
Fiber的结构

  fiber即协程,即执行某个任务的最小单元。

  一个fiber是一个js对象,它包含着一个组件,以及这个组件的输入及输出。

  一个fiber与一个栈帧相对应,但同时也与一个组件的实例相对应。 可以通过代码看一下fiber的数据结构。

type和key

  type是一个any类型,是对自身的一个描述,即有可能为function/class/module,与react的element对应,使其执行的结果可以直接被复用,从而实现了在fiber级别的暂停和恢复。

  type和key的作用同differ算法类似,决定是否需要重新执行该fiber函数。

child和sibling

  这两个属性指向其它的fiber,构成一个fiber的链表。每个child对应该组件的render方法的返回值

  function Parent() {
    return <Child />
  }

  Parent的child属性就与Child相对应。

  sibling支持在render方法中返回多个子节点的数组。

  function Parent() {
    return [<Child1 />, <Child2 />]
  }

  Parent的child属性是Child1,Child1的sibling属性是Child2。

pendingWorkPriority

  pendingWorkPriority的值代表了这个任务的优先级。

  至此,react将任务缩小到fiber单元,通过自行实现fiber构成的函数调用栈,从而控制每个fiber的执行,在这个过程中所有的fiber是一个一个被执行的,而且每个fiber的执行期都是独立可打断的,这样react就实现了逐步构建虚拟函数栈,然后再一次提交到真实的stack中去执行。

Fiber的执行

fiber执行主要分为三个阶段代码

1 beginWork
2 completeWork
3 commitWork

beginWork:即开始一个任务,其中包含了对当前workInProgress的fiber的检查,判断优先级高低等。该方法会被递归调用,直至没有fiber将被执行为止。

completeWork:执行fiber,计算出该fiber的diff,便于在下一个commit阶段使用。

commitWork:调用宿主接口(dom接口),执行渲染。