setState源码

  1. ReactComponent.prototype.setState = function (partialState, callback) {
  2. this.updater.enqueueSetState(this, partialState);
  3. if (callback) {
  4. this.updater.enqueueCallback(this, callback, 'setState');
  5. }
  6. };

入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去。这里我们以对象形式的入参为例,可以看到它直接调用了 this.updater.enqueueSetState 这个方法:

  1. enqueueSetState: function (publicInstance, partialState) {
  2. // 根据 this 拿到对应的组件实例
  3. var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  4. // 这个 queue 对应的就是一个组件实例的 state 数组
  5. var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  6. queue.push(partialState);
  7. // enqueueUpdate 用来处理当前的组件实例
  8. enqueueUpdate(internalInstance);
  9. }

enqueueSetState 做了两件事:

  • 将新的 state 放进组件的状态队列里;
  • 用 enqueueUpdate 来处理将要更新的实例对象。

继续往下走,看看 enqueueUpdate 做了什么:

  1. function enqueueUpdate(component) {
  2. ensureInjected();
  3. // 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
  4. if (!batchingStrategy.isBatchingUpdates) {
  5. // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
  6. batchingStrategy.batchedUpdates(enqueueUpdate, component);
  7. return;
  8. }
  9. // 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
  10. dirtyComponents.push(component);
  11. if (component._updateBatchNumber == null) {
  12. component._updateBatchNumber = updateBatchNumber + 1;
  13. }
  14. }

这个 enqueueUpdate 非常有嚼头,它引出了一个关键的对象——batchingStrategy,该对象所具备的isBatchingUpdates属性直接决定了当下是要走更新流程,还是应该排队等待;其中的batchedUpdates 方法更是能够直接发起更新流程。由此我们可以大胆推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象
接下来,我们就一起来研究研究这个 batchingStrategy。

  1. /**
  2. * batchingStrategy源码
  3. **/
  4. var ReactDefaultBatchingStrategy = {
  5. // 全局唯一的锁标识
  6. isBatchingUpdates: false,
  7. // 发起更新动作的方法
  8. batchedUpdates: function(callback, a, b, c, d, e) {
  9. // 缓存锁变量
  10. var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates
  11. // 把锁“锁上”
  12. ReactDefaultBatchingStrategy. isBatchingUpdates = true
  13. if (alreadyBatchingStrategy) {
  14. callback(a, b, c, d, e)
  15. } else {
  16. // 启动事务,将 callback 放进事务里执行
  17. transaction.perform(callback, null, a, b, c, d, e)
  18. }
  19. }
  20. }

batchingStrategy 对象并不复杂,你可以理解为它是一个“锁管理器”。
这里的“锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。
理解了批量更新整体的管理机制,还需要注意 batchedUpdates 中,有一个引人注目的调用:

  1. transaction.perform(callback, null, a, b, c, d, e)

Transaction 就像是一个“壳子”,它首先会将目标函数用 wrapper(一组 initialize 及 close 方法称为一个 wrapper) 封装起来,同时需要使用 Transaction 类暴露的 perform 方法去执行它。如上面的注释所示,在 anyMethod 执行之前,perform 会先执行所有 wrapper 的 initialize 方法,执行完后,再执行所有 wrapper 的 close 方法。这就是 React 中的事务机制。

流程图

image.png

  • partialState:setState传入的第一个参数,对象或函数
  • _pendingStateQueue:当前组件等待执行更新的state队列
  • isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用
  • dirtyComponent:当前所有处于待更新状态的组件队列
  • transcation:react的事务机制,在被事务调用的方法外包装n个waper对象,并一次执行:waper.init、被调用方法、waper.close
  • FLUSH_BATCHED_UPDATES:用于执行更新的waper,只有一个close方法

    执行步骤

    对照上面流程图的文字说明,大概可分为以下几步:

  • 1.将setState传入的partialState参数存储在当前组件实例的state暂存队列中。

  • 2.判断当前React是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。
  • 3.如果未处于批量更新状态,将批量更新状态标识设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。
  • 4.调用事务的waper方法,遍历待更新组件队列依次执行更新。
  • 5.执行生命周期componentWillReceiveProps。
  • 6.将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将队列置为空。
  • 7.执行生命周期componentShouldUpdate,根据返回值判断是否要继续更新。
  • 8.执行生命周期componentWillUpdate。
  • 9.执行真正的更新,render。
  • 10.执行生命周期componentDidUpdate。

到这里,相信你对 isBatchingUpdates 管控下的批量更新机制已经了然于胸。但是 setState 为何会表现同步这个问题,似乎还是没有从当前展示出来的源码里得到根本上的回答。这是因为 batchedUpdates 这个方法,不仅仅会在 setState 之后才被调用。若我们在 React 源码中全局搜索 batchedUpdates,会发现调用它的地方很多,但与更新流有关的只有这两个地方:

  1. // ReactMount.js
  2. _renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
  3. // 实例化组件
  4. var componentInstance = instantiateReactComponent(nextElement);
  5. // 初始渲染直接调用 batchedUpdates 进行同步渲染
  6. ReactUpdates.batchedUpdates(
  7. batchedMountComponentIntoNode,
  8. componentInstance,
  9. container,
  10. shouldReuseMarkup,
  11. context
  12. );
  13. ...
  14. }

这段代码是在首次渲染组件时会执行的一个方法,我们看到它内部调用了一次 batchedUpdates,这是因为在组件的渲染过程中,会按照顺序调用各个生命周期函数。开发者很有可能在声明周期函数中调用 setState。因此,我们需要通过开启 batch 来确保所有的更新都能够进入 dirtyComponents 里去,进而确保初始渲染流程中所有的 setState 都是生效的。
下面代码是 React 事件系统的一部分。当我们在组件上绑定了事件之后,事件中也有可能会触发 setState。为了确保每一次 setState 都有效,React 同样会在此处手动开启批量更新。

  1. // ReactEventListener.js
  2. dispatchEvent: function (topLevelType, nativeEvent) {
  3. ...
  4. try {
  5. // 处理事件
  6. ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  7. } finally {
  8. TopLevelCallbackBookKeeping.release(bookKeeping);
  9. }
  10. }

话说到这里,一切都变得明朗了起来:isBatchingUpdates 这个变量,在 React 的生命周期函数以及合成事件执行前,已经被 React 悄悄修改为了 true,这时我们所做的 setState 操作自然不会立即生效。当函数执行完毕后,事务的 close 方法会再把 isBatchingUpdates 改为 false。
以 increment 方法为例,整个过程像是这样:

  1. increment = () => {
  2. // 进来先锁上
  3. isBatchingUpdates = true
  4. console.log('increment setState前的count', this.state.count)
  5. this.setState({
  6. count: this.state.count + 1
  7. });
  8. console.log('increment setState后的count', this.state.count)
  9. // 执行完函数再放开
  10. isBatchingUpdates = false
  11. }

很明显,在 isBatchingUpdates 的约束下,setState 只能是异步的。而当 setTimeout 从中作祟时,事情就会发生一点点变化:

  1. reduce = () => {
  2. // 进来先锁上
  3. isBatchingUpdates = true
  4. setTimeout(() => {
  5. console.log('reduce setState前的count', this.state.count)
  6. this.setState({
  7. count: this.state.count - 1
  8. });
  9. console.log('reduce setState后的count', this.state.count)
  10. },0);
  11. // 执行完函数再放开
  12. isBatchingUpdates = false
  13. }

会发现, isBatchingUpdates对 setTimeout 内部的执行逻辑完全没有约束力。因为 isBatchingUpdates 是在同步代码中变化的,而 setTimeout 的逻辑是异步执行的。当 this.setState 调用真正发生的时候,isBatchingUpdates 早已经被重置为了 false,这就使得当前场景下的 setState 具备了立刻发起同步更新的能力。所以咱们前面说的没错——setState 并不是具备同步这种特性,只是在特定的情境下,它会从 React 的异步管控中“逃脱”掉

https://juejin.cn/post/6844903781813993486#heading-12