React进阶笔记【7_setState(异步?同步?)】

2022/4/5 ReactJS

# 异步的动机——批量更新

批量更新是指:每来一个 setState,就把它塞进一个队列里 “攒起来”,等时机成熟,再把 “攒起来” 的 state 结果合并,最后只针对最新的 state 值走一次更新流程。

this.setState({
  count: this.state.count + 1   ===> 入队,[count + 1 的任务]
})
this.setState({
  count: this.state.count + 1   ===> 入队,[count + 1 的任务]
})
this.setState({
  count: this.state.count + 1   ===> 入队,[count + 1 的任务]
})

// --> 合并 state,[count + 1 的任务]
// --> 执行 count + 1 的任务
1
2
3
4
5
6
7
8
9
10
11
12
test = () => {
  for(let i = 0; i < 100; i++){
    this.setState({
      count: this.state.count + 1
    })
  }
}
// 该代码也只会增加任务入队的次数,并不会带来频繁的 Re-render
// 100次结束后也仅仅是 state 的任务队列内容发生了变化,state 本身也不会发生改变
1
2
3
4
5
6
7
8
9

# 同步代码里的 setState

test = () => {
  setTimeout(() => {
     this.setState({
       count: this.state.count + 1
     })
  }, 0)
}
1
2
3
4
5
6
7

那为什么 setTimeout 可以把执行顺序从异步变成同步?

这里要注意的是:并不是 setTimeout 改变了 setState,而是 seTimeout 帮助 setState “逃脱” 了 React 对它的管控,只要是在 React 管控下的 setState,一定是异步的。

# setState 工作流

以下是 React 15 的 setStateFiber 之前的都是如此。

// setState
React.prototype.setState = function(partialState, callback){
  this.updater.enqueueSetState(this, partialState);
  if(callback){
    this.updater.enqueueCallback(this, callback, 'setState');
  } 
}
1
2
3
4
5
6
7
// enqueueSetState
enqueueSetState: function(publicInstance, partialState){
  // 根据 this 拿到对应的组件实例
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  
  // 这个 queue 对应的就是一个组件实例的 state 数组
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState);
  
  // enqueueUpdate 用来处理当前的组件实例
  enqeueUpdate(internalInstance)
}
1
2
3
4
5
6
7
8
9
10
11
12
// enqueueUpdate
function enqueueUpdate(component){
  ensureInjected();
  // 注意这一句是关键,isBatchingUpdates 标识着当前是否处于批量创建/更新组件的阶段
  if(!batchingStrategy.isBatchingUpdates){
    // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 否则先把组件塞入 dirtyComponents 队列里,让它 “再等等”
  dirtyComponents.push(component);
  if(component._updateBatchNumber == null){
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

// 由此可以推断,batchingStrategy 就是 React 内部管理批量更新的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// batchingStrategy 源码
var ReactDefaultBatchingStrategy = {
  // 全局唯一的锁标识,false 表示当前没有进行批量更新操作
  isBatchingUpdates: false,
  // 发起更新动作的方法
  batchedUpdates: function(callback, a, b, c, d, e){
    // 缓存锁变量
    var alreadyBatchingStrategy = ReactDefaultBatchingStrategy.isBatchingUpdates
    // 把锁 “锁上”
    ReactDefaultBatchingStrategy.isBatchingUpdates = true
    
    if(alreadyBatchingStrategy){
      callback(a, b, c, d, e)
    } else {
      // 启用事务,将 callback 放进事务里执行
      transaction.perform(callback, null, a, b, c, d, e)
    }
  }
}
// 每当 React 调用 batchedUpdates 去执行更新动作时,会先把 isBatchingUpdates 这个锁给 “锁上”,表明现在正处于批量更新过程中。当锁被锁上的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队,等待下一次的批量更新,而不能随意插队。
// 这里的任务锁机制,正是 React 面对大量状态,依然能够实现有序分批处理的基石。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# transaction

它是 React 内部的事务机制,以下是官方的注释图:

*                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  1. anyMethod:代表要被包裹的方法
  2. wrapper:代表一层包裹容器,每个 wrapper 可选地包含一个 initialize(前置方法)和一个 close(后置方法),分别会在每次 anyMethod 函数执行前后者执行后执行
  3. perform:执行“ 包裹” 动作的 API,形式为:transaction.perform(anyMethod),表示给 anyMethod 加上一层 wrapper
  4. 可以有多个 wrapper,执行时按照 “包裹” 的顺序,依次执行对应的前置和后置函数。

再看 ReactDefaultBatchingStrategy 这个对象:

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];

// 这两个 wrapper 套进 transaction 的机制里,可以得出:在 callback 执行完之后,RESET_BATCHED_UPDATES 将 isBatchingUpdates 置为 false,FLUSH_BATCHED_UPDATES 会执行 flushBatchedUpdates,然后里面会循环所有 dirtyComponents,调用自己的 updateComponent 来执行所有的生命周期方法,最后实现组件的更新。
1
2
3
4
5
6
7
8
9
10
11
12
13

# 与更新流程相关的 isBatchingUpdates

// ReactMount.js
_renderNewRootComponent: function(nextElement, container, shouldReuseMarkup, context){
  // 实例化组件
  var componentInstance = instantiateReactComponent(nextElement);
  // 初始化渲染直接调用 batchedUpdates 进行同步渲染
  ReactUpdates.batchedUpdates(
  	batchedMountComponentIntoNode,
    componentInstance,
    container,
    shouldReuseMarkup,
    context
  )
  // ...
}

// 该代码会在组件首次渲染时执行,可以看到调用了一次 batchedUpdates,这是因为在组件的渲染过程当中,会按照顺序调用各个生命周期函数,开发者很有可能在生命周期函数中调用 setState,因此需要开启 batch 来确保所有的更新都可以进入 dirtyComponents 中,进而确保初始流程中所有的 setState 都是生效的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ReactEventListener.js
dispatchEvent: function(topLevelType,NativeEvent){
  // ...
  try{
    // 处理事件
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally {
    TopLevelCallbackBookKeeping.release(bookKeeping);
  }
}

// 该代码是 React 的事件系统的一部分,当在组件上绑定了事件,事件也有可能触发 setState,为了确保每一次 setState 都有效,React 会在此处手动开启批量更新。
1
2
3
4
5
6
7
8
9
10
11
12

isBatchingUpdates 在 React 的生命周期函数以及合成事件执行前,已经被 React 悄悄修改为了 true,这时开发者所做的 setState 操作,自然不会立即生效,当函数执行完毕后,事务的 close 方法会再次把 isBatchingUpdates 改为 false

以下是使用和不使用 setTimeout 的伪代码:

// 默认情况下的 setState
test = () => {
  // 进来先锁上
  isBatchingUpdates = true
  this.setState({
    count: this.state.count + 1
  })
  // 执行完之后再解锁放开
  isBatchingUpdates = false
}

// 使用 setTimeout 后
test = () => {
   // 进来先锁上
  isBatchingUpdates = true
  setTimeout(() => {
     this.setState({
       count: this.state.count + 1
     })
  }, 0)
  // 执行完之后再解锁放开
  isBatchingUpdates = false
}

// 可以看出来默认情况下,setState 只能是异步的,而有 setTimeout 的话,setState 的逻辑就会异步执行,而此时 isBatchingUpdates 早已经被修改为 false 了!这也就是为什么 setTimeout 里的 setState 具备当下立刻发起同步更新的能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 总结

setState 的表现会因为场景的不同而不同:

  • 在 React 的钩子函数及合成事件中,它表现为 异步。
  • 在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为 同步。