setState 是同步还是异步的呢? | 蛋烘糕
蛋烘糕.

不写博客的工程师不是好的搬砖工🧱

setState 是同步还是异步的呢?

Cover Image for setState 是同步还是异步的呢?
蛋烘糕
蛋烘糕

写在前面的话

setState 是 React 很重要的模块, 社区中也有很多分析文章,大多强调 setState 是异步更新,但有些文章分析又说某些情况下是同步更新,那到底是同步还是异步呢,这篇文章还是基于 15.x进行的分析,16.x 的分析等后面用机会再分享。

我们看一下 React 官网(React.Component – React)对 setState 的说明:

官网也没说 setState 到底是同步还是异步,只是说 React 不保证 setState 之后能够立即拿到改变后的结果。

我们先看一个经典例子:

// demo.js
class Demo extends PureComponent {
  state = {
    count: 0,
  };
  componentDidMount() {
    console.log("pre state", this.state.count);
    this.setState({
      count: this.state.count + 1,
    });
    console.log("next state", this.state.count);

    //测试setTimeout
    setTimeout(() => {
      console.log("setTimeout pre state", this.state.count);
      this.setState({
        count: this.state.count + 1,
      });
      console.log("setTimeout next state", this.state.count);
    }, 0);
  }

  onClick = (event) => {
    // 测试合成函数中setState
    console.log(`${event.type} pre state`, this.state.count);
    this.setState({
      count: this.state.count + 1,
    });
    console.log(`${event.type} next state`, this.state.count);
  };

  render() {
    return <button onClick={this.onClick}>count+1</button>;
  }
}

这里有三种方法调用 setState:

  • 在 componentDidMount 中直接调用 setState;
  • 在 componentDidMount 的 setTimeout 方法里调用 setState;
  • 在 dom 中绑定 onClick(React 的合成函数:抹平不同浏览器和端的差异)直接调用 setState;

从控制台打印出来的结果看,方法 1 和 3 直接调用 setState 是异步的,而方法 2 中 setTimeout 调用 setState 证明了同步,到底为什么呢?这两种调用方式有什么区别嘛?接下来我们从源码进行分析。

源码分析

1、setState 入口函数
//ReactComponent.js
ReactComponent.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ?
  "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.') : invariant(false)
  : undefined;
  if ("development" !== 'production') {
    "development" !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : undefined;
  }
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback);
  }
};

//ReactUpdateQueue.js
enqueueSetState: function(publicInstance, partialState) {
  // 根据 this.setState 中的 this 拿到内部实例, 也就是组件实例
  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState'
  );

  if (!internalInstance) {
    return;
  }

  //取得组件实例的_pendingStateQueue
  var queue =
    internalInstance._pendingStateQueue ||
    (internalInstance._pendingStateQueue = []);
  //将partial state存到_pendingStateQueue
  queue.push(partialState);
  //唤起enqueueUpdate
  enqueueUpdate(internalInstance);
};
...

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

在 setState 函数中调用 enqueueSetState, 拿到内部组件实例, 然后把要更新的 partial state 存到其_pendingStateQueue 中,至此,setState 调用方法执行结束,接下来是 setState 调用之后的动作。

2、调用 setState 后发生了什么?

setState 调用之后执行方法 enqueueUpdate

//ReactUpdates.js
function enqueueUpdate(component) {
  //注入默认策略,开启ReactReconcileTransaction事务
  ensureInjected();
  // 如果没有开启batch(或当前batch已结束)就开启一次batch再执行, 这通常发生在异步回调中调用 setState
  //batchingStrategy:批量更新策略,通过事务的方式实现state的批量更新
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 如果batch已经开启,则将该组件保存在 dirtyComponents 中存储更新
  dirtyComponents.push(component);
}

上面 demo 对 setState 三次调用结果之所以不同,应该是这里的判断逻辑导致的:

  • 1 和 3 的调用走的是 isBatchingUpdates === true 分支,没有执行更新操作;
  • 2 的 setTimeout 走的是 isBatchingUpdates === false 分支,执行更新;

isBatchingUpdates 是事务 batchingStrategy 的一个标记,如果为 true,把当前调用 setState 的组件放入 dirtyComponents 数组中,做存储处理,不会立即更新,如果为 false,将 enqueueUpdate 作为参数传入 batchedUpdates 方法中,在 batchedUpdates 中执行更新操作。

可是事务 batchingStrategy 到底是做什么的呢?batchedUpdates 又做了什么处理?我们看一下它的源码:

//ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction(); // 实例化事务

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    // 开启一次batch
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      callback(a, b, c, d, e);
    } else {
      // 启动事务, 将callback放进事务里执行
      transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};
//说明:这里使用到了事务transaction,简单来说,transaction就是将需要执行的方法使用 wrapper 封装起来,
//再通过事务提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法,
//执行完 perform 之后(即执行method 方法后)再执行所有的 close 方法。
//一组 initialize 及 close 方法称为一个 wrapper。事务支持多个 wrapper 叠加,嵌套,
//如果当前事务中引入了另一个事务B,则会在事务B完成之后再回到当前事务中执行close方法。

ReactDefaultBatchingStrategy 就是一个批量更新策略事务, isBatchingUpdates 默认是 false,而 batchedUpdates 方法被调用时才会将属性 isBatchingUpdates 设置为 true,表明目前处于批量更新流中;可是上面 demo 中 1 和 3 执行到判断逻辑之前源码分析中没见到有 batchedUpdates 方法调用,那 batchedUpdates 什么时候被调用的呢?

全局搜索 React 中调用 batchedUpdates 的地方很多,分析后发现与更新流程相关的只有两个地方:

// ReactMount.js
_renderNewRootComponent: function(nextElement,container,shouldReuseMarkup,context) {
  ...
  // 实例化组件
  var componentInstance = instantiateReactComponent(nextElement, null);
  //初始渲染是同步的,但在渲染期间发生的任何更新,在componentWillMount或componentDidMount中,将根据当前的批处理策略进行批处理
  ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    container,
    shouldReuseMarkup,
    context
  );
  ...
},
// ReactEventListener.js
dispatchEvent: function (topLevelType, nativeEvent) {
  ...
  try {
    // 处理事件
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally {
    TopLevelCallbackBookKeeping.release(bookKeeping);
  }
}
  • 第一种情况,是在首次渲染组件时调用 batchedUpdates,开启一次 batch。因为组件在渲染的过程中, 会依顺序调用各种生命周期函数, 开发者很可能在生命周期函数中(如 componentWillMount 或者 componentDidMount)调用 setState. 因此, 开启一次 batch 就是要存储更新(放入 dirtyComponents), 然后在事务结束时批量更新. 这样以来, 在初始渲染流程中, 任何 setState 都会生效, 用户看到的始终是最新的状态

  • 第二种情况,如果在组件上绑定了事件,在绑定事件中很有可能触发 setState,所以为了存储更新(dirtyComponents),需要开启批量更新策略。在回调函数被调用之前, React 事件系统中的 dispatchEvent 函数负责事件的分发, 在 dispatchEvent 中启动了事务, 开启了一次 batch, 随后调用了回调函数. 这样一来, 在事件的监听函数中调用的 setState 就会生效.

这里借用《深入 REACT 技术栈》文章里的一个在 componentDidMount 中 setState 的调用栈图例:

图例中表明,ReactDefaultBatchingStrategy.batchedUpdates 在 ReactMount._renderNewRootComponent 中被调用,依次倒推,最后发现在组件首次渲染时就会通过 injectBatchingStrategy()方法注入 ReactDefaultBatchingStrategy(这部分有兴趣可以看一下 ReactDefaultInjection.js 源码),并且在 ReactMount.render 中触发_renderNewRootComponent 函数,调用 batchedUpdates 将 isBatchingUpdates 设置为了 true,所以 componentDidMount 的执行都是在一个大的事务 ReactDefaultBatchingStrategyTransaction 中。

这就解释了在 componentDidMount 中调用 setState 并不会立即更新 state,因为正处于一个这个大的事务中,isBatchingUpdates 此时为 true,所以只会放入 dirtyComponents 中等待稍后更新。

3、state 什么时候批量更新呢?

追踪代码后我画了一个组件初次渲染和 setState 后简单的事务启动和执行的顺序:

从上面的图中可以看到,ReactDefaultBatchingStrategy 就是一个批量更新策略事务,控制了批量策略的生命周期。看一下 ReactDefaultBatchingStrategy 源码分析一下事务中执行了什么:

// ReactDefaultBatchingStrategy.js
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
  • 在事务的 close 阶段执行了 flushBatchedUpdates 函数,flushBatchedUpdates 执行完之后再将 ReactDefaultBatchingStrategy.isBatchingUpdates 重置为 false,表示这次 batch 更新结束。
  • flushBatchedUpdates 函数启动 ReactUpdatesFlushTransaction 事务,这个事务开启了批量更新,执行 runBatchedUpdates 对 dirtyComponents 循环处理。
4、怎么批量更新的呢?

批量更新 flushBatchedUpdates 中,看一下源码:

// ReactUpdates.js
var flushBatchedUpdates = function () {
  // 开启批量更新
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }
    // 批量处理callback
    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};

flushBatchedUpdates 开启事务 ReactUpdatesFlushTransaction, 执行 runBatchedUpdates,

// ReactUpdates.js
function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  // 排序保证父组件优于子组件更新
  dirtyComponents.sort(mountOrderComparator);
  // 遍历dirtyComponents
  for (var i = 0; i < len; i++) {
    var component = dirtyComponents[i];
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
    // 执行更新操作
    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction
    );
    // 存储callbacks
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance()
        );
      }
    }
  }
}

接下来就是 ReactReconciler 调用组件实例的 performUpdateIfNecessary 方法,这里只分析 ReacrCompositeComponent 实例,如果接收了 props,就会调用 receiveComponent 方法,在该方法里调用 updateComponent 方法;如果有新的要更新的状态(_pendingStateQueue 不为空)也会直接调用 updateComponent 来更新:

// ReactCompositeComponent.js
performUpdateIfNecessary: function(transaction) {
  if (this._pendingElement != null) {
    ReactReconciler.receiveComponent(
      this,
      this._pendingElement || this._currentElement,
      transaction,
      this._context
    );
  }
  // 待更新state队列不为空或者_pendingForceUpdate为true
  if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
    this.updateComponent(
      transaction,
      this._currentElement,
      this._currentElement,
      this._context,
      this._context
    );
  }
},

调用组件实例中的 updateComponent,这块代码是组件更新机制的核心,负责管理生命周期中的 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate;

这段代码比较多,集中在 ReactCompositeComponent.js 文件中,

如果不想看源码可以直接看后面的代码流程图:

//ReactCompositeComponent.js
updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext
  ) {
    var inst = this._instance;

    var nextContext = this._context === nextUnmaskedContext ?
      inst.context :
      this._processContext(nextUnmaskedContext);
    var nextProps;

    // Distinguish between a props update versus a simple state update
    if (prevParentElement === nextParentElement) {
      // Skip checking prop types again -- we don't read inst.props to avoid
      // warning for DOM component props in this upgrade
      nextProps = nextParentElement.props;
    } else {
      nextProps = this._processProps(nextParentElement.props);
      // 如果有接收新的props,执行componentWillReceiveProps 方法,
      if (inst.componentWillReceiveProps) {
        inst.componentWillReceiveProps(nextProps, nextContext);
      }
    }
    // 合并props
    var nextState = this._processPendingState(nextProps, nextContext);
    // 执行shouldComponentUpdate判断是否需要更新
    var shouldUpdate =
      this._pendingForceUpdate ||
      !inst.shouldComponentUpdate ||
      inst.shouldComponentUpdate(nextProps, nextState, nextContext);
    ...
    // 如果需要更新执行_performComponentUpdate,否则只将当前的props和state保存下来,不做更新
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`, `this.state` and `this.context`.
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext
      );
    } else {
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  },
  ...
  // 执行componentWillUpdate
  _performComponentUpdate: function(
    nextElement,
    nextProps,
    nextState,
    nextContext,
    transaction,
    unmaskedContext
  ) {
    var inst = this._instance;

    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
    var prevProps;
    var prevState;
    var prevContext;
    if (hasComponentDidUpdate) {
      prevProps = inst.props;
      prevState = inst.state;
      prevContext = inst.context;
    }

    if (inst.componentWillUpdate) {
      inst.componentWillUpdate(nextProps, nextState, nextContext);
    }

    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;

    this._updateRenderedComponent(transaction, unmaskedContext);

    if (hasComponentDidUpdate) {
      transaction.getReactMountReady().enqueue(
        inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
        inst
      );
    }
  }
// 执行unmountComponent,_instantiateReactComponent, mountComponent、render
_updateRenderedComponent: function(transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    var nextRenderedElement = this._renderValidatedComponent();
    // 如果prevRenderedElement, nextRenderedElement相等只执行receiveComponent
    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      ReactReconciler.receiveComponent(
        prevComponentInstance,
        nextRenderedElement,
        transaction,
        this._processChildContext(context)
      );
    } else {
      // prevRenderedElement, nextRenderedElement不相等,则执行旧组件的unmountComponent
      var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);
      ReactReconciler.unmountComponent(prevComponentInstance);
      this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);
      // 组件实例化_instantiateReactComponent
      this._renderedComponent = this._instantiateReactComponent(
        nextRenderedElement
      );
      // 组件挂载
      var nextMarkup = ReactReconciler.mountComponent(
        this._renderedComponent,
        transaction,
        this._nativeParent,
        this._nativeContainerInfo,
        this._processChildContext(context)
      );
      // 新组件替换旧组件
      this._replaceNodeWithMarkup(oldNativeNode, nextMarkup);
    }
  },
5、updateComponent 流程图

6、demo 扩展

上面分析了一个很经典的 demo,下面看一下原生事件和 async 事件中 setState 调用后的表现。

(1) 绑定原生事件,调用 setState
class Button extends PureComponent {
  state = {
    count: 0,
    val: 0,
  };
  componentDidMount() {
    // 测试原生方法:手动绑定mousedown事件
    console.log("mousedown pre state", this.state.count);
    ReactDOM.findDOMNode(this).addEventListener(
      "mousedown",
      this.onClick.bind(this)
    );
    console.log("mousedown pre state", this.state.count);
  }

  onClick(event) {
    console.log(`${event.type} pre state`, this.state.count);
    this.setState({
      count: this.state.count + 1,
    });
    console.log(`${event.type} next state`, this.state.count);
  }

  render() {
    return <button onClick={this.onClick.bind(this)}>count+1</button>;
  }
}

控制台

(2) async 函数和 sleep 函数
class Button extends PureComponent {
  state = {
    count: 0,
    val: 0,
  };
  async componentDidMount() {
    // 测试async函数中setState
    for (let i = 0; i < 1; i++) {
      console.log("sleep pre state", this.state.count);
      await sleep(0);
      this.setState({
        count: this.state.count + 1,
      });
      console.log("sleep next state", this.state.count);
    }
  }

  asyncClick = () => {
    this.setState({
      count: this.state.count + 1,
    });
  };

  async onClick(event) {
    const type = event.type;
    console.log(`${type} pre state`, this.state.count);
    await this.asyncClick();
    console.log(`${type} next state`, this.state.count);
  }

  render() {
    return <button onClick={this.onClick.bind(this)}>count+1</button>;
  }
}

控制台

7、结论
  • setState 在生命周期函数和合成函数中都是异步更新。
  • setState 在 steTimeout、原生事件和 async 函数中都是同步更新。每次更新不代表都会触发 render,如果 render 内容与 newState 有关联,则会触发,否则即便 setState 多次也不会 render
  • 如果 newState 内容与 render 有依赖关系,就不建议同步更新,因为每次 render 都会完整的执行一次批量更新流程(只是 dirtyComponets 长度为 1,stateQueue 也只有该组件的 newState),调用一次 diff 算法,这样会影响 React 性能。
  • 如果没有必须同步渲染的理由,不建议使用同步,会影响 react 渲染性能
8、总结

React 整个更新机制处处包含着事务,总的来说,组件的更新机制依靠事务进行批量更新;

  • 一次 batch(批量)的生命周期就是从 ReactDefaultBatchingStrategy 事务 perform 之前(调用 ReactUpdates.batchUpdates)到这个事务的最后一个 close 方法调用后结束;
  • 事务启动后, 遇到 setState 则将 partial state 存到组件实例的_pendingStateQueue 上, 然后将这个组件存到 dirtyComponents 数组中, 等到 ReactDefaultBatchingStrategy 事务结束时调用 runBatchedUpdates 批量更新所有组件;
  • 组件的更新是递归的, 三种不同类型的组件都有自己的 updateComponent 方法来决定自己的组件如何更新, 其中 ReactDOMComponent 会采用 diff 算法对比子元素中最小的变化, 再批量处理.
  • 生命周期函数和合成函数中调用 setState 表现异步更新,是因为组件初始化和调用合成函数时都会触发 ReactDefaultBatchingStrategy 事务的 batchUpdates 方法,将批量更新标记设置为 true,所以后面的 setState 都会存储到 dirtyComponents 中,执行批量更新之后再将标志设置为 false;
  • setTimeout、原生事件和 async 函数中调用 setState 表现同步更新,是因为遇到这些函数时不会触发 ReactDefaultBatchingStrategy 事务的 batchUpdates 方法,所以批量更新标记依旧时 false,所以表现为同步。

补充:transaction 事务介绍

React 的事务机制比较简单,包括三个阶段,initialize、perform 和 close,并且事务之间支持叠加。

事务提供了一个 mixin 方法供其他模块实现自己需要的事务。而要使用事务的模块,除了需要把 mixin 混入自己的事务实现中外,还要额外实现一个抽象的 getTransactionWrappers 接口。这个接口用来获取所有需要封装的前置方法(initialize)和收尾方法(close),因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。

这里看一个《深入 React 技术栈》文章中的例子就比较好理解了

var Transaction = require('./Transaction');
// 我们自己定义的事务
var MyTransaction = function() {
// ... };
Object.assign(MyTransaction.prototype, Transaction.Mixin, { getTransactionWrappers: function() {
  return [{
    initialize: function() {
    console.log('before method perform'); },
    close: function() {
    console.log('after method perform');
    }
  }];
 };
});
var transaction = new MyTransaction(); var testMethod = function() {
console.log('test'); }
transaction.perform(testMethod);
// 打印的结果如下:
// before method perform // test
// after method perform
JavaScript this 对象查找
JavsScript 原型
博客日历
2022年10月
26
27
28
29
30
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
01
02
03
04
05
06
更多