React系列之 setState 的核心实现

React源码系列

在开始之前,先奉上本次文章的demo,点击这里,进行跳转。

前言

相信你已经对React框架各种语法了然于心,能够熟练的应用于各种业务场景,造各种轮子,甚至孰能生巧,各种性能优化,信手拈来。

如何在了解源码后,可以实现具有相同能力的setState呢?

在使用React类组件的时候,你是否会有疑问:

疑问一:为什么我连续调用setState后,打印日志代码显示都是同一个值。
疑问二:为什么setState()接收一个函数后,打印日志,显示就正常了。
疑问三:为什么调用setState是异步执行的。

带着这些疑问,请继续往下看

请看下面代码:

  onClickBtn() {
    const { count1 } = this.state;//count1--> 默认值为0
    this.setState({ count1: count1 + 1});
    this.setState({ count1: count1 + 1});
    this.setState({ count1: count1 + 1});
    this.setState({ count1: count1 + 1});
  }
复制代码

如下面GIF图所示,当点击按钮时,调用onClickBtn函数时,明明调用了四次setState,按照我们同步的理解的话,每次点击,都应该增加4,但是为什么每次只是增加1

2.gif
按照我们的理解,当一个对象连续执行调用同一个函数时,我们都会给他做一个防抖,把得到的值,进行合并,那是不是React中也是这样的。

image.png

翻开源码部分,找到react/packages/react-reconciler/src/ReactUpdateQueue.old.js,可以看见当update.tagUpdateState类型时,会调用Object.assign函数进行merge

    //...
    case UpdateState: {
      const payload = update.payload;
      let partialState;
      if (typeof payload === 'function') {
        partialState = payload.call(instance, prevState, nextProps);
      } else {
        partialState = payload;
      }
      if (partialState === null || partialState === undefined) {
        return prevState;
      }
      return Object.assign({}, prevState, partialState);
复制代码

所以两者上还是有相似之处的,区别在于,一个使用宏任务,一个使用微任务。

React的官网文档上,有专门介绍如何处理这种情况:

image.png
从上述代码中也可以看出,首先判断payload是否是一个函数,当条件为真,调用payload,同时,传入prevState

通过payload函数的返回新的state,当通过 Object.assign进行合并时,得到最新状态。

反之,当payload不是一个函数时,此时React,尚未将最新的state赋值给组件实例,因此,每次通过this.state获取的状态,一直都是之前的状态,所以会把payload合并,最终只打印了最后一次调用。

我们都知道setState函数是个异步函数,那在React中,setState是如何做到异步的呢?

二、React中的异步

1.gif

通过翻阅源码,当点击的时候,将performSyncWorkOnRoot存入syncQueue队列,执行微任务。
以下是React异步调用的核心代码。判断当前浏览器是否支持queueMicrotask(微任务),当浏览器不支持queueMicrotask时,通过返回Promise.resolve(null).then(fn)函数,做兼容。

export const scheduleMicrotask: any =
  typeof queueMicrotask === 'function'
    ? queueMicrotask
    : typeof Promise !== 'undefined'
    ? callback =>
        Promise.resolve(null)
          .then(callback)
          .catch(handleErrorInNextTick)
    : scheduleTimeout; // TODO: Determine the best fallback here.

function handleErrorInNextTick(error) {
  setTimeout(() => {
    throw error;
  });
}
复制代码

MDN中对于微任务和宏任务的解释是:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。

换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

image.png
所以,同理,在代码在执行的时候,先将任务放入队列中, 当调用scheduleMicrotask函数时,清空其队列。

那我们如何去实现一个具体相同功能的函数呢?

三、如何实现

setState需要具备两个能力:

  • setState可以接收一个函数。
  • setState的是异步的,具备回调函数。

1、首先创建一个Component函数,用来初始化一些基本参数,定义一个setState函数,当调用setState函数时,调用enqueueSetstate函数,对partialStatecallback进行保存。

//初始化
function Component(props) {
  this.props = props;
  this.updatequeue = [];
}
//定义setState函数
Component.prototype.setState = function(partialState, callback) {
  this.enqueueSetstate(partialState, callback);
};
复制代码

2、对于enqueueSetstate函数

我们需要它将每次调用的setState的值保存在队列中,以便在合适的时机,进行调用。

此时还需要定义一个函数scheduleMicrotask,判断updatequeue数组为空的时候,进行调用,同时将flushUpdateQueue函数传入进去,等待调用。

3、flushUpdateQueue函数

可以理解成React源码中的flushSyncCallbackQueue函数。

Component.prototype.enqueueSetstate = function(partialState, callback) {
  if (this.updatequeue.length === 0) {
    this.scheduleMicrotask(this.flushUpdateQueue.bind(this));
  }
  const state = {payload: partialState, callback}
  this.updatequeue.push(state);

};
复制代码

4、对于scheduleMicrotask

上面关于源码部分,也已经解释过了,为了完成flushUpdateQueue函数的调用,此处写法略有不同。

Component.prototype.scheduleMicrotask = function(callback) {

  return typeof queueMicrotask === "function"
    ? queueMicrotask(callback)
    : typeof callback === "function"
    ? Promise.resolve(null).then(callback)
    : setTimeout(callback);
};
复制代码

5、重要的是flushUpdateQueue函数

该函数需要完成两件事情,第一件事,首先判断payload是否是一个函数,条件为真,调用payload,并且传入prevStateprops,然后进行赋值。

条件为假,直接返回对象。

最后将各个条件判断得到的payload和prevState进行合并。

Object.assign(prevState, partialState);
复制代码

注意
我们需要在每一次while循环中,对prevState进行重新赋值,保证prevState为上一次循环结束时的最新状态,才可以在后续payload为函数时,传入的prevState为前一个状态。

6、对于队列,队列的规则是先入先出

因此,处理updatequeue队列时,我们需要取出updatequeue中的第一个值,然后再进行判断。而且需要设置一个条件,该条件为判断updatequeue队列是否为空时,停止while循环。

最终,调用render函数,渲染页面,执行回调函数。

//清空队列,并render
Component.prototype.flushUpdateQueue = function() {
  let item;
  while (item = this.updatequeue.shift()) {
    let { payload, callback } = item;
    let prevState = this.state;
    let partialState;
    if (typeof payload !== "function") {
      partialState = payload;
    } else {
      partialState = payload.call(this, prevState, this.props);
    }
    // 合并
    Object.assign(prevState, partialState);
    const newState = prevState;
    
    this.state = newState;

    this.render();
 
    if(typeof callback === 'function') {
      callback.call(this);
    }
  };

};

复制代码

看下成果,是否满足我们的要求。

444.gif

结果基本和React中的setState保持一致。

最后,点击这里,进入demo

四、结束

整个过程,最重要的核心点在于使用微任务进行回调并清空队列。

我们需要考虑的是,传入微任务的函数,会在事件循环中什么时候开始调用,是在开始之前,还是开始之后进行调用。

写此文章的目的,还是在于对事件循环(eventLoop)的掌握,以及场景应用。如果能举一反三,那么对我们以后的编程思路也会大有提升。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享