相关文章推荐
打盹的刺猬  ·  how to get selected ...·  1 年前    · 
唠叨的紫菜汤  ·  az nginx deployment ...·  1 年前    · 
坚强的机器猫  ·  Django 2.1.7 ...·  1 年前    · 

Fiber设计思想

Fiber 是对 React 核心算法的重构,facebook 团队使用两年多的时间去重构 React 的核心算法,在React16 以上的版本中引入了 Fiber 架构,其中的设计思想是非常值得我们学习的。

为什么需要Fiber

我们知道,在浏览器中,页面是一帧一帧绘制出来的,渲染的帧率与设备的刷新率保持一致。一般情况下,设备的屏幕刷新率为1s 60次,当每秒内绘制的帧数(FPS)超过60时,页面渲染是流畅的;而当FPS小于60时,会出现一定程度的卡顿现象。下面来看完整的一帧中,具体做了哪些事情:

  • 首先需要处理输入事件,能够让用户得到最早的反馈
  • 接下来是处理定时器,需要检查定时器是否到时间,并执行对应的回调
  • 接下来处理 Begin Frame(开始帧),即每一帧的事件,包括 window.resize、scroll、media query change 等
  • 接下来执行请求动画帧 requestAnimationFrame(rAF),即在每次绘制之前,会执行 rAF 回调
  • 紧接着进行 Layout 操作,包括计算布局和更新布局,即这个元素的样式是怎样的,它应该在页面如何展示
  • 接着进行 Paint 操作,得到树中每个节点的尺寸与位置等信息,浏览器针对每个元素进行内容填充
  • 到这时以上的六个阶段都已经完成了,接下来处于空闲阶段(Idle Peroid),可以在这时执行 requestIdleCallback 里注册的任务(后面会详细讲到这个 requestIdleCallback ,它是 React Fiber 实现的基础)
  • js引擎和页面渲染引擎是在同一个渲染线程之内,两者是互斥关系。如果在某个阶段执行任务特别长,例如在定时器阶段或 Begin Frame 阶段执行时间非常长,时间已经明显超过了16ms,那么就会阻塞页面的渲染,从而出现卡顿现象。

    在 react16 引入 Fiber 架构之前,react 会采用递归对比虚拟DOM树,找出需要变动的节点,然后同步更新它们,这个过程 react 称为 reconcilation (协调)。在 reconcilation 期间,react 会一直占用浏览器资源,会导致用户触发的事件得不到响应。实现的原理如下所示:

    这里有7个节点,B1、B2 是 A1 的子节点,C1、C2 是 B1 的子节点,C3、C4 是 B2 的子节点。传统的做法就是采用深度优先遍历去遍历节点,具体代码如下:

    const root = {
      key: 'A1',
      children: [{
        key: 'B1',
        children: [{
          key: 'C1',
          children: []
          key: 'C2',
          children: []
        key: 'B2',
        children: [{
          key: 'C3',
          children: []
          key: 'C4',
          children: []
    const walk = dom => {
      console.log(dom.key)
      dom.children.forEach(child => walk(child))
    walk(root)
    

    这种遍历是递归调用,执行栈会越来越深,而且不能中断,中断后就不能恢复了。递归如果非常深,就会十分卡顿。如果递归花了100ms,则这100ms浏览器是无法响应的,代码执行时间越长卡顿越明显。传统的方法存在不能中断和执行栈太深的问题。

    因此,为了解决以上的痛点问题,React希望能够彻底解决主线程长时间占用问题,于是引入了 Fiber 来改变这种不可控的现状,把渲染/更新过程拆分为一个个小块的任务,通过合理的调度机制来调控时间,指定任务执行的时机,从而降低页面卡顿的概率,提升页面交互体验。通过Fiber架构,让reconcilation过程变得可被中断。适时地让出CPU执行权,可以让浏览器及时地响应用户的交互。

    React16中使用了 Fiber,但是 Vue 是没有 Fiber 的,为什么呢?原因是二者的优化思路不一样:

  • Vue 是基于 template 和 watcher 的组件级更新,把每个更新任务分割得足够小,不需要使用到 Fiber 架构,将任务进行更细粒度的拆分
  • React 是不管在哪里调用 setState,都是从根节点开始更新的,更新任务还是很大,需要使用到 Fiber 将大任务分割为多个小任务,可以中断和恢复,不阻塞主进程执行高优先级的任务
  • 下面,让我们走进 Fiber 的世界,看看具体是怎么实现的。

    什么是Fiber

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

    一个执行单元

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

    首先 React 向浏览器请求调度,浏览器在一帧中如果还有空闲时间,会去判断是否存在待执行任务,不存在就直接将控制权交给浏览器,如果存在就会执行对应的任务,执行完成后会判断是否还有时间,有时间且有待执行任务则会继续执行下一个任务,否则就会将控制权交给浏览器。这里会有点绕,可以结合上述的图进行理解。

    Fiber 可以被理解为划分一个个更小的执行单元,它是把一个大任务拆分为了很多个小块任务,一个小块任务的执行必须是一次完成的,不能出现暂停,但是一个小块任务执行完后可以移交控制权给浏览器去响应用户,从而不用像之前一样要等那个大任务一直执行完成再去响应用户。

    一种数据结构

    Fiber 还可以理解为是一种数据结构,React Fiber 就是采用链表实现的。每个 Virtual DOM 都可以表示为一个 fiber,如下图所示,每个节点都是一个 fiber。一个 fiber包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性,React Fiber 机制的实现,就是依赖于以下的数据结构。在下文中会讲到基于这个链表结构,Fiber 究竟是如何实现的。

    PS:这里需要说明一下,Fiber 是 React 进行重构的核心算法,fiber 是指数据结构中的每一个节点,如下图所示的A1、B1都是一个 fiber。

    requestAnimationFrame

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

    例如我想让浏览器在每一帧中,将页面 div 元素的宽变长1px,直到宽度达到100px停止,这时就可以采用requestAnimationFrame来实现这个功能。

    <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) } else { console.log(allInterval) // 打印requestAnimationFrame的全部时间间隔 btn.addEventListener('click', () => { div.style.width = 0 let currrent = Date.now() start = currrent requestAnimationFrame(progress) console.log(allInterval) </script>

    浏览器会在每一帧中,将div的宽度变宽1px,知道到达100px为止。打印出每一帧的时间间隔如下,大约是16ms左右。

    requestIdleCallback

    requestIdleCallback 也是 react Fiber 实现的基础 api 。我们希望能够快速响应用户,让用户觉得够快,不能阻塞用户的交互,requestIdleCallback能使开发者在主事件循环上执行后台和低优先级的工作,而不影响延迟关键事件,如动画和输入响应。正常帧任务完成后没超过16ms,说明有多余的空闲时间,此时就会执行requestIdleCallback里注册的任务。

    具体的执行流程如下,开发者采用requestIdleCallback方法注册对应的任务,告诉浏览器我的这个任务优先级不高,如果每一帧内存在空闲时间,就可以执行注册的这个任务。另外,开发者是可以传入timeout参数去定义超时时间的,如果到了超时时间了,浏览器必须立即执行,使用方法如下:window.requestIdleCallback(callback, { timeout: 1000 })。浏览器执行完这个方法后,如果没有剩余时间了,或者已经没有下一个可执行的任务了,React应该归还控制权,并同样使用requestIdleCallback去申请下一个时间片。具体的流程如下图:

    window.requestIdleCallback(callback)callback中会接收到默认参数 deadline ,其中包含了以下两个属性:

  • timeRamining 返回当前帧还剩多少时间供用户使用
  • didTimeout 返回 callback 任务是否超时
  • requestIdleCallback 方法非常重要,下面分别讲两个例子来理解这个方法,在每个例子中都需要执行多个任务,但是任务的执行时间是不一样的,下面来看浏览器是如何分配时间执行这些任务的:

    直接执行task1、task2、task3,各任务的时间均小于16ms:

    let taskQueue = [
      () => {
        console.log('task1 start')
        console.log('task1 end')
      () => {
        console.log('task2 start')
        console.log('task2 end')
      () => {
        console.log('task3 start')
        console.log('task3 end')
    const performUnitWork = () => {
      // 取出第一个队列中的第一个任务并执行
      taskQueue.shift()()
    const workloop = (deadline) => {
      console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
      // 如果此帧剩余时间大于0或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
      // 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
      while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
        performUnitWork()
      // 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
      if (taskQueue.length > 0) {
        window.requestIdleCallback(workloop, { timeout: 1000 })
    requestIdleCallback(workloop, { timeout: 1000 })
    

    上面定义了一个任务队列taskQueue,并定义了workloop函数,其中采用 window.requestIdleCallback(workloop, { timeout: 1000 })去执行taskQueue中的任务。每个任务中仅仅做了console.log的工作,时间是非常短的,浏览器计算此帧中还剩余15.52ms,足以一次执行完这三个任务,因此在此帧的空闲时间中,taskQueue中定义的三个任务均执行完毕。打印结果如下:

    在task1、task2、task3中加入睡眠时间,各自执行时间超过16ms:

    const sleep = delay => {
      for (let start = Date.now(); Date.now() - start <= delay;) {}
    let taskQueue = [
      () => {
        console.log('task1 start')
        sleep(20) // 已经超过一帧的时间(16.6ms),需要把控制权交给浏览器
        console.log('task1 end')
      () => {
        console.log('task2 start')
        sleep(20) // 已经超过一帧的时间(16.6ms),需要把控制权交给浏览器
        console.log('task2 end')
      () => {
        console.log('task3 start')
        sleep(20) // 已经超过一帧的时间(16.6ms),需要把控制权交给浏览器
        console.log('task3 end')
    

    基于以上的例子做了部分改造,让taskQueue中的每个任务的执行时间都超过16.6ms,看打印结果知道浏览器第一帧的空闲时间为14ms,只能执行一个任务,同理,在第二帧、第三帧的时间也只够执行一个任务。所有这三个任务分别是在三帧中分别完成的。打印结果如下:

    浏览器一帧的时间并不严格是16ms,是可以动态控制的(如第三帧剩余时间为49.95ms)。如果子任务的时间超过了一帧的剩余时间,则会一直卡在这里执行,直到子任务执行完毕。如果代码存在死循环,则浏览器会卡死。如果此帧的剩余时间大于0(有空闲时间)或者已经超时(上文定义了 timeout 时间为1000,必须强制执行了),且当时存在任务,则直接执行该任务。如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器。如果多个任务执行总时间小于空闲时间的话,是可以在一帧内执行多个任务的。

    Fiber链表结构设计

    Fiber结构是使用链表实现的,Fiber tree实际上是个单链表树结构,详见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)。插入节点单元时需要考虑是否已经存在节点,如果不存在直接将firstUpdatelastUpdate指向此节点即可。更新队列是遍历这个链表,根据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
    

    最后写一个demo,实例化一个队列,向其中加入很多节点,再更新这个队列:

    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 }
    

    Fiber节点设计

    Fiber 的拆分单位是 fiber(fiber tree上的一个节点),实际上就是按虚拟DOM节点拆,我们需要根据虚拟dom去生成 Fiber 树。下文中我们把每一个节点叫做 fiber 。fiber 节点结构如下,源码详见ReactInternalTypes.js

    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 节点包括了以下的属性:

    (1)type & key

  • fiber 的 type 和 key 与 React 元素的作用相同。fiber 的 type 描述了它对应的组件,对于复合组件,type 是函数或类组件本身。对于原生标签(div,span等),type 是一个字符串。随着 type 的不同,在 reconciliation 期间使用 key 来确定 fiber 是否可以重新使用。
  • (2)stateNode

  • stateNode 保存对组件的类实例,DOM节点或与 fiber 节点关联的其他 React 元素类型的引用。一般来说,可以认为这个属性用于保存与 fiber 相关的本地状态。
  • (3)child & sibling & return

  • child 属性指向此节点的第一个子节点(大儿子)。
  • sibling 属性指向此节点的下一个兄弟节点(大儿子指向二儿子、二儿子指向三儿子)。
  • return 属性指向此节点的父节点,即当前节点处理完毕后,应该向谁提交自己的成果。如果 fiber 具有多个子 fiber,则每个子 fiber 的 return fiber 是 parent 。
  • 所有 fiber 节点都通过以下属性:child,sibling 和 return来构成一个 fiber node 的 linked list(后面我们称之为链表)。如下图所示:

    其他的属性还有memoizedState(创建输出的 fiber 的状态)、pendingProps(将要改变的 props )、memoizedProps(上次渲染创建输出的 props )、pendingWorkPriority(定义 fiber 工作优先级)等等,在这里就不展开描述了。

    Fiber执行原理

    从根节点开始渲染和调度的过程可以分为两个阶段:render 阶段、commit 阶段。

  • render 阶段:这个阶段是可中断的,会找出所有节点的变更
  • commit 阶段:这个阶段是不可中断的,会执行所有的变更
  • render阶段

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

    React Fiber首先是将虚拟DOM树转化为Fiber tree,因此每个节点都有childsiblingreturn属性,遍历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 = B1
    B1.sibling = B2
    B1.child = C1
    C1.sibling = C2
    B2.child = C3
    C3.sibling = C4
    module.exports = A1
    

    写遍历方法:

    let rootFiber = require('./element')
    const beginWork = (Fiber) => {
      console.log(`${Fiber.key} start`)
    const completeUnitWork = (Fiber) => {
      console.log(`${Fiber.key} end`)
    // 遍历函数
    const performUnitOfWork = (Fiber) => {
      beginWork(Fiber)
      if (Fiber.child) {
        return Fiber.child
      while (Fiber) {
        completeUnitWork(Fiber)
        if (Fiber.sibling) {
          return Fiber.sibling
        Fiber = Fiber.return
    const workloop = (nextUnitOfWork) => {
      // 如果有待执行的执行单元则执行,返回下一个执行单元
      while (nextUnitOfWork) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
      if (!nextUnitOfWork) {
        console.log('reconciliation阶段结束')
    workloop(rootFiber)
    

    打印结果:

    A1 start
    B1 start
    C1 start
    C1 end // C1完成
    C2 start
    C2 end // C2完成
    B1 end // B1完成
    B2 start
    C3 start
    C3 end // C3完成
    C4 start
    C4 end // C4完成
    B2 end // B2完成
    A1 end // A1完成
    reconciliation阶段结束
    

    收集effect list

    知道了遍历方法之后,接下来需要做的工作就是在遍历过程中,收集所有节点的变更产出effect list,注意其中只包含了需要变更的节点。通过每个节点更新结束时向上归并effect list来收集任务结果,最后根节点的effect list里就记录了包括了所有需要变更的结果。

    收集effect list的具体步骤为:

  • 如果当前节点需要更新,则打tag更新当前节点状态(props, state, context等)
  • 为每个子节点创建fiber。如果没有产生child fiber,则结束该节点,把effect list归并到return,把此节点的sibling节点作为下一个遍历节点;否则把child节点作为下一个遍历节点
  • 如果有剩余时间,则开始下一个节点,否则等下一次主线程空闲再开始下一个节点
  • 如果没有下一个节点了,进入pendingCommit状态,此时effect list收集完毕,结束。
  • 收集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 = {
          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 list
    const 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.lastEffect = 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 // 没有弟弟,则找到父亲,让父亲完成,父亲会去找他的弟弟即叔叔
    

    commit阶段

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

    根据一个 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 更新视图

    写一个递归函数,从根节点出发,根据effect list完成全部更新:

    const commitRoot = () => {
      let currentFiber = workInProgressRoot.firstEffect
      while (currentFiber) {
        commitWork(currentFiber)
        currentFiber = currentFiber.nextEffect
      currentRoot = workInProgressRoot // 把当前渲染成功的根fiber赋给currentRoot
      workInProgressRoot = null
    

    完成视图更新

    接下来定义循环执行工作,当计算完成每个 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 })
    

    到这时,已经根据收集到的变更信息,完成了视图的刷新操作。

    本文是为了让大家对 React Fiber 能有一个大致的了解,本文介绍了为什么在 React 中要引入 Fiber 机制,它的设计思想是什么,以及在代码中是如何一点点实现的。但是仍然有很多的点没有覆盖到,例如如何定义调度任务优先级、如何进行任务中断与断点恢复……感兴趣的朋友可以结合 react 源码继续研究。

  •