React16——看看setState过程中fiber干了什么事情

4,305 阅读8分钟

目的

通过观察调用栈和其他博客的介绍亲身体验下setState过程中fiber干了什么事情

1、create-react-app创建一个demo

下图是一个典型的create-react-app创建的项目,其中Text.js是我新增的子组件,在App.js中引用到。

clipboard.png

2、修改App.js文件和新增Text.js文件

App.js

import React, { Component } from 'react';
import { Text } from './Text'

class App extends Component {
    constructor(props) {
        super(props)
        this.state = {
            tab: 'Welcome to React'
        }
    }
    updateState = () => {
        this.setState(() => ({tab: 'Bye Bye'}))
    }
  render() {
    return (
      <div className="App">
          <Text tab={this.state.tab} updateState={this.updateState} />
      </div>
    );
  }
}
export default App;

Text.js

import React from 'react'

export const Text = (props) => {
    return (
        <span onClick={() => props.updateState()}>{props.tab}</span>
    )
}

3、执行setState

state.tab的初始值是'Welcome to React',在setState中,传入了一个箭头函数,去更新state.tab的值为'Bye Bye',

//partialState为() => ({tab: ''Bye Bye}),callback没有传入
Component.prototype.setState = function (partialState, callback) {
  //this为当前组件的实例
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

接着,执行了updater上的enqueueSetState方法,每一个实例都会有一个updater(更新器),updater的作用在下面介绍,在当前App组件实例中,_reactInternalFiber是当前组件的fiber,而_reactInternalInstance是在react15使用的对象。

clipboard.png

4、updater更新器

updater有3个方法,只用关心enqueueSetState

var updater = {
    isMounted: isMounted,
    /*
    * instance: 上一步传入的App组件实例,
    * partialState:需要执行更新的箭头函数,
    * callback:undefined
    */
    enqueueSetState: function (instance, partialState, callback) {
      //获取到当前实例上的fiber
      var fiber = get(instance);
      //计算当前fiber的到期时间(优先级)
      var expirationTime = computeExpirationForFiber(fiber);
      //一次更新需要的配置参数
      var update = {
        expirationTime: expirationTime, //优先级
        partialState: partialState, //更新的state,通常是函数而不推荐对象写法
        callback: callback, //更新之后执行的回调函数
        isReplace: false, //
        isForced: false, //是否强制更新
        capturedValue: null, //捕获的值
        next: null, //
      };
      //将update上需要更新的信息添加到fiber中
      insertUpdateIntoFiber(fiber, update);
      //调度器调度fiber任务
      scheduleWork(fiber, expirationTime);
    },
    //替换更新state,不关注
    enqueueReplaceState: function (instance, state, callback) {},
    //执行强制更新state,不关注
    enqueueForceUpdate: function (instance, callback) {}
  };

下面按步骤详细看看这个函数内部的执行流程。

获取fiber:key === instance,fiber很重要,记录了很多有用的信息,比如当前组件实例的各种属性和状态、优先级、标识等。

function get(key) {
  return key._reactInternalFiber;
}

也许你会很好奇fiber长什么样,图上展示的是组件实例上注入的fiber数据结构。

clipboard.png

计算到期时间:也就是计算当前fiber任务的优先级,从代码上看需要判断的条件比较多,既可以是异步更新,也可以是同步更新。在当前测试中,进入的是同步更新的流程。而同步对应的优先级就是1,所以expirationTime = 1。

 //用来计算fiber的到期时间,到期时间用来表示任务的优先级。
 function computeExpirationForFiber(fiber) {
    var expirationTime = void 0;
    if (expirationContext !== NoWork) {
      // 
      expirationTime = expirationContext;
    } else if (isWorking) {
      if (isCommitting) {
        //同步模式,立即处理任务,默认是1
        expirationTime = Sync;
      } else {
        //渲染阶段的更新应该与正在渲染的工作同时过期。
        expirationTime = nextRenderExpirationTime;
      }
    } else {
      //没有到期时间的情况下,创建一个到期时间
      if (fiber.mode & AsyncMode) {
        if (isBatchingInteractiveUpdates) {
          // 这是一个交互式更新
          var currentTime = recalculateCurrentTime();
          expirationTime = computeInteractiveExpiration(currentTime);
        } else {
          // 这是一个异步更新
          var _currentTime = recalculateCurrentTime();
          expirationTime = computeAsyncExpiration(_currentTime);
        }
      } else {
        // 这是一个同步更新
        expirationTime = Sync;
      }
    }
    if (isBatchingInteractiveUpdates) {
      //这是一个交互式的更新。跟踪最低等待交互过期时间。这允许我们在需要时同步刷新所有交互更新。
      if (lowestPendingInteractiveExpirationTime === NoWork || expirationTime > lowestPendingInteractiveExpirationTime) {
        lowestPendingInteractiveExpirationTime = expirationTime;
      }
    }
    return expirationTime;
  }

将update上需要更新的信息添加到fiber中:这个函数的作用就是把我们在上面通过计算之后得到的update更新到fiber上面,实际操作是对象的赋值,跟合并是一个意思。

function insertUpdateIntoFiber(fiber, update) {
  //确保更新队列存在,不存在则创建
  ensureUpdateQueues(fiber);
  //上一步已经将q1和q2队列进行了处理,定义2个局部变量queue1和queue2来保存队列信息。
  var queue1 = q1;
  var queue2 = q2;
  // 如果只有一个队列,请将更新添加到该队列并退出。
  if (queue2 === null) {
    insertUpdateIntoQueue(queue1, update);
    return;
  }

  // 如果任一队列为空,我们需要添加到两个队列中。
  if (queue1.last === null || queue2.last === null) {
    //将update的值更新到队列1和队列2上,然后退出该函数
    insertUpdateIntoQueue(queue1, update);
    insertUpdateIntoQueue(queue2, update);
    return;
  }

  // 如果两个列表都不为空,则由于结构共享,两个列表的最后更新都是相同的。所以,我们应该只追加到其中一个列表。
  insertUpdateIntoQueue(queue1, update);
  // 但是我们仍然需要更新queue2的`last`指针。
  queue2.last = update;
}

初始化的时候,fiber中的updateQueue是null,这时候,就要创建createUpdateQueue一个更新队列。alternate本质上也是fiber,它记录的是上一次setState操作的fiber,同时alternate又是fiber的一个属性。

clipboard.png

ensureUpdateQueues的作用是确保更新队列不为null。

var q1 = void 0;
var q2 = void 0;
function ensureUpdateQueues(fiber) {
  q1 = q2 = null;
  // 我们将至少有一个和最多两个不同的更新队列。
  //alternate是fiber上的一个属性,初始化是null,执行了setState的过程中,会将当前的FiberNode保存到alternate上,下次setState时,就能读取到,可以用来做状态回滚。
  var alternateFiber = fiber.alternate;
  var queue1 = fiber.updateQueue;
  if (queue1 === null) {
    // 没有队列,就创建队列
    queue1 = fiber.updateQueue = createUpdateQueue(null);
  }

  var queue2 = void 0;
  if (alternateFiber !== null) {
    queue2 = alternateFiber.updateQueue;
    if (queue2 === null) {
      queue2 = alternateFiber.updateQueue = createUpdateQueue(null);
    }
  } else {
    queue2 = null;
  }
  queue2 = queue2 !== queue1 ? queue2 : null;

  // 使用模块变量
  q1 = queue1;
  q2 = queue2;
}
//将update的值更新到queue中。
function insertUpdateIntoQueue(queue, update) {
  // 将更新附加到列表的末尾。
  if (queue.last === null) {
    // 队列是空的
    queue.first = queue.last = update;
  } else {
    queue.last.next = update;
    queue.last = update;
  }
  if (queue.expirationTime === NoWork || queue.expirationTime > update.expirationTime) {
    queue.expirationTime = update.expirationTime;
  }
}

scheduleWork进行调度:上面的几个步骤记得做了什么吗?拿到组件实例上的fiber,然后通过计算得到优先级和其他需要更新的fiber属性,最后更新到fiber上,同时创建了更新队列。但是react还没开始干活是不是,更新队列有了,fibre也有了,react的大脑该对fiber进行调度了。

调度的逻辑很复杂,因为影响因素太多了,我无法一一列举,只能根据当前的调用栈识别用到的部分。

//传入2个参数,fiber和优先级,内部又嵌套了一个函数scheduleWorkImpl,这个函数才是逻辑部分。
function scheduleWork(fiber, expirationTime) {
    return scheduleWorkImpl(fiber, expirationTime, false);
  }

请注意当前传入的fiber是合并了update属性之后的fiber。

scheduleWorkImpl有写让人迷惑,司徒大佬的文章也没有解释清楚这个函数,一步步来看的话,recordScheduleUpdate的作用是先判断当前有没有正在提交更新或者已经在更新中的任务,应该是等updater执行完后,要用到的一些条件预设。

然后将node = fiber,别纠结为什么是直接相等,接着执行循环,当前node也就是fiber不为空,根据条件,要在循环过程中对node清空,清空之后退出函数。那么,这个清空的过程做了什么事情呢?

先是判断node里面的到期时间是不是等于NoWork,NoWork表示的是0,它表示的是当前没有在调度中的fiber,然后判断node的到期时间是不是大于传入的到期时间,如果满足条件,就将node的到期时间更新为新传入的到期时间。

然后判断alternate不为空的情况下,alternate在没有执行过setState,通常是初始化的时候是空状态,当执行过一次setState之后,就会将旧的FiberNode赋值给alternate,下面的函数中,如果alternate不为空,并且expirationTime和上一个if的判断一致的情况下,就更新alternate中的expirationTime。

上2个条件是更新到期时间的,第3个条件是判断return是不是等于null,return的含义在完全理解fiber一文中有说到,表示当前的fiber任务向谁提交。在本demo中,当前是第一次执行,所有它的return为null。

function scheduleWorkImpl(fiber, expirationTime, isErrorRecovery) {
    //记录调度的状态
    recordScheduleUpdate();
    var node = fiber;
    while (node !== null) {
      // 将父路径移到根目录并更新每个节点的到期时间。
      if (node.expirationTime === NoWork || node.expirationTime > expirationTime) {
        node.expirationTime = expirationTime;
      }
      if (node.alternate !== null) {
        if (node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime) {
          node.alternate.expirationTime = expirationTime;
        }
      }
      if (node['return'] === null) {
        if (node.tag === HostRoot) {
          var root = node.stateNode;
          if (!isWorking && nextRenderExpirationTime !== NoWork && expirationTime < nextRenderExpirationTime) {
            // 是一个中断。 (用于性能跟踪。)
            interruptedBy = fiber;
            resetStack();
          }
          if (
          // 如果我们处于渲染阶段,我们不需要为此更新安排此根目录,因为我们将在退出之前执行此操作。
          !isWorking || isCommitting ||
          // ......除非这是与我们渲染的根不同的root。
          nextRoot !== root) {
            // 将root添加到root调度
            requestWork(root, expirationTime);
          }
        } else {
          return;
        }
      }
      node = node['return'];
    }
  }
//顾名思义,记录调度的状态
function recordScheduleUpdate() {
  if (enableUserTimingAPI) {
    if (isCommitting) {
      //当前是否有正在提交的调度任务,肯定没有啦。
      hasScheduledUpdateInCurrentCommit = true;
    }
    //currentPhase表示当前执行到哪个生命周期了。
    if (currentPhase !== null && currentPhase !== 'componentWillMount' && currentPhase !== 'componentWillReceiveProps') {
      //当前是否有调度到某个生命周期阶段的任务
      hasScheduledUpdateInCurrentPhase = true;
    }
  }
}

到这里为止,updater的函数执行完了,我们总结一下它到底做了什么事情。一共有4点:

  • 找到实例上的fiber
  • 计算得到当前fiber的优先级
  • 将要更新的fiber推送到更新队列
  • 根据fiber树上的优先级确定更新工作,从当前fiber的return为起点,开始递归,直至到达根节点,根节点的return=null。

后续

根据调用栈,我们看到了setState函数的执行过程,但是此时并没有在浏览器上看到更新,因为具体的调度工作还是要依靠react的核心算法去执行,updater只是将fiber更新到队列中,和确定了更新的优先级。

后面要经历react的事件合成,Diff算法,虚拟DOM解析,生命周期执行等过程。非常多的代码,要是一一解释,可以写一本书出来了。

以后要是有时间,可以将后半部分关于setState里的() => ({tab: 'Bye Bye'})是如何更新的说说。 一切都要从createWorkInProgress(current, pendingProps, expirationTime)开始说起。

无论是国内哪位大神的博客,只要是介绍fiber的,万变不离其宗,看国外的这篇文章:fiber详解

Bye Bye,各位。

卧槽,这是什么

clipboard.png