# 异步的动机——批量更新
批量更新是指:每来一个 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
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
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
2
3
4
5
6
7
那为什么 setTimeout
可以把执行顺序从异步变成同步?
这里要注意的是:并不是 setTimeout
改变了 setState
,而是 seTimeout
帮助 setState
“逃脱” 了 React 对它的管控,只要是在 React 管控下的 setState
,一定是异步的。
# setState 工作流
以下是 React 15 的 setState
,Fiber
之前的都是如此。

// 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
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
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
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
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- anyMethod:代表要被包裹的方法
- wrapper:代表一层包裹容器,每个 wrapper 可选地包含一个 initialize(前置方法)和一个 close(后置方法),分别会在每次 anyMethod 函数执行前后者执行后执行
- perform:执行“ 包裹” 动作的 API,形式为:
transaction.perform(anyMethod)
,表示给 anyMethod 加上一层 wrapper - 可以有多个 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
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
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
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
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 原生事件中,它都表现为 同步。