setState到底是异步还是同步的呢

131 阅读4分钟

旧版本的setState

对于React15来说,我们用一个例子来看看

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  componentDidMount() {
    // 第一次调用
    this.setState({ val: this.state.val + 1 });
    console.log('first setState', this.state);

    // 第二次调用
    this.setState({ val: this.state.val + 1 });
    console.log('second setState', this.state);

    // 第三次调用
    this.setState({ val: this.state.val + 1 }, () => {
      console.log('in callback', this.state)
    });
  }
  render() {
    return <div> val: { this.state.val } </div>
  }
}

export default App;

对于本例子,最后state的值只会是1,而且输出的时候应该是以下这样的:

我们只有在对应的回调函数中才可以获取到最新的值,那么我们可能会误认为setState是异步操作

但是实际上setState 是一次同步操作,只是每次操作之后并没有立即执行,而是将 setState 进行了缓存,mount 流程结束或事件操作结束,才会拿出所有的 state 进行一次计算。如果 setState 脱离了 React 的生命周期或者 React 提供的事件流,setState 之后就能立即拿到结果。

在进一步学习之前,我们需要学习一下React相关的事务

从源码出发

我们将会从React15开始探查

我们React中组件的setState方法是挂载在原型上面的

// 对外暴露的 React.Component
function ReactComponent() {
  this.updater = ReactUpdateQueue;
}
// setState 方法挂载到原型链上
ReactComponent.prototype.setState = function (partialState, callback) {
  // 调用 setState 后,会调用内部的 updater.enqueueSetState
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

var ReactUpdateQueue = {
  enqueueSetState(component, partialState) {
    // 在组件的 _pendingStateQueue 上暂存新的 state
    if (!component._pendingStateQueue) {
      component._pendingStateQueue = [];
    }
    var queue = component._pendingStateQueue;
    queue.push(partialState);
    enqueueUpdate(component);
  },
  enqueueCallback: function (component, callback, callerName) {
    // 在组件的 _pendingCallbacks 上暂存 callback
    if (component._pendingCallbacks) {
      component._pendingCallbacks.push(callback);
    } else {
      component._pendingCallbacks = [callback];
    }
    enqueueUpdate(component);
  }
}

我们可以发现,在调用setState后,会调用updater.enqueSetState函数

根据updater.enqueSetState函数,我们可以发现在调用我们这个updater.enqueSetState函数时并没有直接更新state,而是将传入的参数放在组件内部的_pendingStateQueue队列中(代码21行),之后再去调用enqueueUpdate(component);(代码22行)来执行更新的过程。

那么我们需要对于enqueueUpdate(component)进行进一步探索

var dirtyComponents = [];
function enqueueUpdate(component) {
  if (!batchingStrategy.isBatchingUpdates) {
          // 开始进行批量更新
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 如果在更新流程,则将组件放入脏组件队列,表示组件待更新
  dirtyComponents.push(component);
}

我们可以看到enqueueUpdate首先会通过batchingStrategy.isBatchingUpdates去判断当前是否在更新流程中,不在的话那么会调用batchingStrategy.batchedUpdates(enqueueUpdate, component)进行更新;在流程中,会push到dirtyComponents中进行缓存。

那么我们需要进一步对于batchingStrategy进行进一步探索了。

batchingStrategy是React进行批处理的一种策略,该策略的实现基于Transaction

我们需要对于 Transaction有一点了解,因此我们需要对于 Transaction的源码进行相关查看

class Transaction {
  reinitializeTransaction() {
    this.transactionWrappers = this.getTransactionWrappers();
  }
  perform(method, scope, ...param) {
    this.initializeAll(0);
    var ret = method.call(scope, ...param);
    this.closeAll(0);
    return ret;
  }
   initializeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.initialize.call(this);
    }
  }
    closeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.close.call(this);
    }
  }
}

Transaction 通过 perform 方法启动,然后通过扩展的 getTransactionWrappers 获取一个数组,该数组内存在多个 wrapper 对象,每个对象包含两个属性:initializeclose。perform 中会先调用所有的 wrapper.initialize,然后调用传入的回调,最后调用所有的 wrapper.close

batchingStrategy相关代码如下:

var transaction = new ReactDefaultBatchingStrategyTransaction();

var batchingStrategy = {
  // 判断是否在更新流程中
  isBatchingUpdates: false,
  // 开始进行批量更新
  batchedUpdates: function (callback, component) {
    // 获取之前的更新状态
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
      // 将更新状态修改为 true
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      // 如果已经在更新状态中,等待之前的更新结束
      return callback(callback, component);
    } else {
      // 进行更新
      return transaction.perform(callback, null, component);
    }
  }
};

class ReactDefaultBatchingStrategyTransaction extends Transaction {
  constructor() {
    this.reinitializeTransaction()
  }
  getTransactionWrappers () {
    return [
      {
        initialize: () => {},
        close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
      },
      {
        initialize: () => {},
        close: () => {
          ReactDefaultBatchingStrategy.isBatchingUpdates = false;
        }
      }
    ]
  }
}

启动事务可以拆分成三步来看:

  1. 先执行 wrapper 的 initialize,此时的 initialize 都是一些空函数,可以直接跳过;

  2. 然后执行 callback(也就是 enqueueUpdate),执行 enqueueUpdate 时,由于已经进入了更新状态,batchingStrategy.isBatchingUpdates 被修改成了 true,所以最后还是会把 component 放入脏组件队列,等待更新;

  3. 后面执行的两个 close 方法,第一个方法的 flushBatchedUpdates 是用来进行组件更新的,第二个方法用来修改更新状态,表示更新已经结束。

其中flushBatchedUpdates 里面会取出所有的脏组件队列进行 diff,最后更新到 DOM。

function flushBatchedUpdates() {
  if (dirtyComponents.length) {
    runBatchedUpdates()
  }
};

function runBatchedUpdates() {
  // 省略了一些去重和排序的操作
  for (var i = 0; i < dirtyComponents.length; i++) {
    var component = dirtyComponents[i];

    // 判断组件是否需要更新,然后进行 diff 操作,最后更新 DOM。
    ReactReconciler.performUpdateIfNecessary(component);
  }
}

performUpdateIfNecessary() 会调用 Component.updateComponent()

setState拿不到同步数据的原因

按照源码逻辑,setState 的时候,batchingStrategy.isBatchingUpdatesfalse 会开启一个事务,将组件放入脏组件队列,最后进行更新操作,而且这里都是同步操作。讲道理,setState 之后,我们可以立即拿到最新的 state。

然而,事实并非如此,在 React 的生命周期及其事件流中,batchingStrategy.isBatchingUpdates 的值早就被修改成了 true

在组件 mount 和事件调用的时候,都会调用 batchedUpdates,这个时候已经开始了事务,所以只要不脱离 React,不管多少次 setState 都会把其组件放入脏组件队列等待更新。一旦脱离 React 的管理,比如在 setTimeout 中,setState 就可以马上拿到对应的更新后数据了。

如何立马获取setState更新后的数据

  1. 使用setTimeout,通过setTimeout去摆脱React的事务管理

    componentDidMount(){ setTimeout(() => { this.setState({value:this.state.value+1}); console.log(this.state.value); this.setState({value:this.state.value+1}); console.log(this.state.value); this.setState({value:this.state.value+1}); console.log(this.state.value); this.setState({value:this.state.value+1}); console.log(this.state.value); },0) }

  2. 使用setState的回调函数,即传入第二个参数

    this.setState(({value}=>{ value:value+1 }),()=>{ console.log(this.state.value); });

  3. 使用Promise,通过Promise去将对应的setState操作包起来,然后使用then去获取数据

    setStatePromise(updator) { return new Promise( function (resolve,reject){ this.setState(updator,resolve); }.bind(this)) } componentDidMount(){ this.setStatePromise(({value}) => ({ value:value+1 })).then(() => { console.log(this.state.value); }); }

对于React16中的useState

我们通过例子来看看

function Component() {
  const [a, setA] = useState(1)
  const [b, setB] = useState('b')
  console.log('render')

  function handleClickWithPromise() {
    Promise.resolve().then(() => {
      setA((a) => a + 1)
      setB('bb')
    })
  }

  function handleClickWithoutPromise() {
    setA((a) => a + 1)
    setB('bb')
  }

  return (
    <Fragment>
      <button onClick={handleClickWithPromise}>
        {a}-{b} 异步执行
      </button>
      <button onClick={handleClickWithoutPromise}>
        {a}-{b} 同步执行
      </button>
    </Fragment>
  )
}

点击同步执行时,只render了一次

点击异步执行时,render了两次

上面的情况是连续执行两个useState

我们来看看连续执行两次同一个useState看看

function Component() {
  const [a, setA] = useState(1)
  console.log('a', a)

  function handleClickWithPromise() {
    Promise.resolve().then(() => {
      setA((a) => a + 1)
      setA((a) => a + 1)
    })
  }

  function handleClickWithoutPromise() {
    setA((a) => a + 1)
    setA((a) => a + 1)
  }

  return (
    <Fragment>
      <button onClick={handleClickWithPromise}>{a} 异步执行</button>
      <button onClick={handleClickWithoutPromise}>{a} 同步执行</button>
    </Fragment>
  )
}

点击同步执行时,只render了一次,但是打印了3,证明两次setA都执行

点击异步执行时,render了两次,两次setA各自render了一次,分别打印了2,3

Tip:同步执行时useState也会对state进行逐个处理,而setState则只会处理最后一次

class Component extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      a: 1,
    }
  }

  handleClickWithPromise = () => {
    Promise.resolve().then(() => {
      this.setState({a: this.state.a + 1})
      this.setState({a: this.state.a + 1})
    })
  }

  handleClickWithoutPromise = () => {
    this.setState({a: this.state.a + 1})
    this.setState({a: this.state.a + 1})
  }

  render() {
    console.log('a', this.state.a)
    return (
      <Fragment>
        <button onClick={this.handleClickWithPromise}>异步执行</button>
        <button onClick={this.handleClickWithoutPromise}>同步执行</button>
      </Fragment>
    )
  }
}

当点击同步执行按钮时,两次 setState 合并,只执行了最后一次,打印 2

当点击异步执行按钮时,两次 setState 各自 render 一次,分别打印 2,3setState到底是异步还是同步的呢