React essence note - 11 -React setState 到底是同步的, 还是异步的?
React setState 到底是同步的, 还是异步的?
经典题目
import React from 'react'
import { Button } from 'antd'
interface StateType {
count: number
name?: string
}
class SetStateDemo extends React.Component<any, StateType> {
state = {
count: 0,
}
// + 1
increment = () => {
console.info('increment setState前的count', this.state.count)
this.setState({
count: this.state.count + 1,
})
console.info('increment setState后的count', this.state.count)
}
// +3
triple = () => {
console.info('triple setState前的count', this.state.count)
this.setState({
count: this.state.count + 1,
})
this.setState({
count: this.state.count + 1,
})
this.setState({
count: this.state.count + 1,
})
console.info('triple setState后的count', this.state.count)
}
// + 1
reduce = () => {
setTimeout(() => {
console.info('reduce setState前的count', this.state.count)
this.setState({
count: this.state.count - 1,
})
console.info('reduce setState后的count', this.state.count)
}, 0)
}
render() {
return (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-around',
margin: 50,
}}
>
<Button type="primary" onClick={this.increment}>
增加
</Button>
<Button type="default" onClick={this.triple}>
三倍
</Button>
<Button type="primary" onClick={this.reduce}>
减少
</Button>
</div>
<div>{this.state.count}</div>
</div>
)
}
}
export default SetStateDemo
// 分别点击 增加, 三倍, 减少的运行结果
// increment setState前的count 0
// increment setState后的count 0
// triple setState前的count 1
// triple setState后的count 1
// reduce setState前的count 2
// reduce setState后的count 1
异步的动机和原理 - 批量更新的艺术
- 在
setState调用之后,都发生了什么?

- 从上图可以看出,一个完整的更新流程, 涉及了包括
re-render在内的多个步骤,re-render本身涉及对DOM的操作, 带来的开销非常大,假如说"一次 setState 就触发一个完整的更新流程"这个结论成立, 那么每一次 setState 的调用都会触发一次 re-render, 我们的视图很可能没刷新几次就卡死了.
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
this.setState({
count: this.state.count + 1 ===> shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate
});
- 事实上,
setState异步的一个重要的动机 -避免频繁的re-render - 在实际的
React运行时,setState异步的实现方式有点类似于Vue的$nextTick和浏览器的Event-Loop: 每来一个setState, 就把它塞进一个队列里"攒起来",等到时机成熟,再把攒起来的state结果做合并,最后只针对最新的state走一次更新流程 ===> 批量更新
this.setState({
count: this.state.count + 1 // 入队, [count+1的任务]
})
this.setState({
count: this.state.count + 1 // 入队, [count+1的任务,count+1的任务]
})
this.setState({
count: this.state.count + 1 // 入队, [count+1的任务,count+1的任务,count+1的任务]
})
// 合并 state, [count+3]的任务 => 执行 count+3的任务
- 只要同步代码还在执行, "攒起来"这个动作就不会停止.
"同步现象"背后的故事:从源码角度看 setState 工作流
reduce = () => {
setTimeout(() => {
console.log('reduce setState前的count', this.state.count)
this.setState({
count: this.state.count - 1
});
console.log('reduce setState后的count', this.state.count)
},0);
}
- 从题目上看, setState 似乎是在 setTimeout 函数的"保护"之下, 才有了同步这一"特异功能". 事实也的确如此, 假如我们把 setTimeout 摘掉, setState 前后的 console 表现将会与 increment 方法中无异
- 这里先给出一个结论, 并不是 setTimeout改变了setState, 而是 setTimeout 帮助 setState "逃脱"了 React 对它的管控, 只要是在React管控下的setState, 一定是异步的
解读 setState 工作流

// 入口函数
ReactComponent.prototype.setState = function(partialState, callback){
this.updater.enqueueSetState(this,partialState)
if(callback){
this.updater.enqueueCallback(this, callback, 'setState')
}
}
/*
入口函数在这里就是充当一个分发器的角色, 根据入参的不同, 将其分发到不同的功能函数中去.
这里我们以对象形式的入参为例, 可以看到它直接调用了 this.updater.enqueueSetState 这个方法
*/
// enqueueSetState 函数
const enqueueSetState = function (publicInstance, partialState){
// 根据 this 拿到对应的组件实例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState')
// 这个 queue 对应的就是一个组件实例的 state 数组
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate 用来处理当前的组件实例
enqueueUpdate(internalInstance);
}
// 这里做了两件事:
// 1: 将新的 state 放进组件的状态队列中
// 2: 用enqueueUpdate来处理将要更新的实例对象
const enqueueUpdate = function(component){
ensureInjected();
// 注意这一句是问题的关键, isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
if (!batchingStrategy.isBatchingUpdates) {
// 若当前没有处于批量创建/更新组件的阶段, 则立即更新组件
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 否则.现把组件塞入 dirtyComponents队列里, 让它再等等
dirtyComponents.push(component)
if(component._updateBatchNumber === null){
component._updateBatchNumber = updateBatchNumber + 1
}
}
/*
enqueueUpdate 引出了一个关键的对象——batchingStrategy,该对象所具备的 isBatchingUpdates 属性直接决定了当下是要走更新流程, 还是应该排队等待
其中的 batchedUpdates 方法更是能够直接发起更新流程
由此我们可以大胆推测, batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象
*/
// batchingStrategy 源码
var ReactDefaultBatchingStrategy = {
// 全局唯一的锁标识
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)
}
}
}
/*
batchingStrategy 对象并不复杂, 你可以理解为它是一个"锁管理器"
这里的"锁", 是指 React 全局唯一的 isBatchingUpdates 变量, isBatchingUpdates 的初始值是 false, 意味着"当前并未进行任何批量更新操作". 每当 React 调用 batchedUpdate 去执行更新动作时, 会先把这个锁给"锁上"(置为 true), 表明"现在正处于批量更新过程中". 当锁被"锁上"的时候, 任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新, 而不能随意"插队". 此处体现的"任务锁"的思想, 是 React 面对大量状态仍然能够实现有序分批处理的基石
*/
理解 React 中的 Transaction(事务) 机制
Transaction在React源码中的分布可以说非常广泛. 如果你在 DebugReact项目的过程中, 发现函数调用栈中出现了initialize、perform、close、closeAll或者notifyAll这样的方法名, 那么很可能你当前就处于一个Transaction中.Transaction在React源码中表现为一个核心类,React官方曾经这样描述它:Transaction是创建一个黑盒, 该黑盒能够封装任何的方法. 因此, 那些需要在函数运行前、后运行的方法可以通过此方法封装(即使函数运行中有异常抛出, 这些固定的方法仍可运行), 实例化Transaction时只需提供相关的方法即可.

- 说白了,
Transaction就像是一个'壳子', 它首先会将目标函数用wrapper(一组initialize及close方法称为一个wrapper) 封装起来, 同时需要使用Transaction类暴露的perform方法去执行它. 如上面的注释所示, 在anyMethod执行之前,perform会先执行所有wrapper的initialize方法, 执行完后, 再执行所有wrapper的close方法. 这就是React中的事务机制.
同步现象 的本质
ReactDefaultBatchingStrategy对象: 其实就是一个批量更新策略事务, 它的wrapper有两个,FLUSH_BATCHED_UPDATES和RESET_BATCHED_UPDATES
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 的执行机制里, 不难得出一个这样的流程

- 到这里, 相信你对 isBatchingUpdates 管控下的批量更新机制已经了然于胸. 但是 setState 为何会表现同步这个问题, 似乎还是没有从当前展示出来的源码里得到根本上的回答. 这是因为 batchingUpdates 这个方法, 不仅仅会在 setState 之后才被调用. 若我们在 React 源码中全局搜索 batchingUpdates, 会发现调用它的地方很多, 但与更新流有关的只有这两个地方
// 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 都是生效的
*/
//下面代码是 React 事件系统的一部分.当我们在组件上绑定了事件之后, 事件中也有可能会触发 setState. 为了确保每一次 setState 都有效, React 同样会在此处手动开启批量更新
// ReactEventListener.js
dispatchEvent: function (topLevelType, nativeEvent) {
...
try {
// 处理事件
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
/**
* 到这里, 一切都变得明朗了起来:
* isBatchingUpdates 这个变量, 在 React 的生命周期函数以及合成事件执行前, 已经被 React 悄悄修改为了 true, 这时我们所做的 setState 操作自然不会立即生效.
* 当函数执行完毕后, 事务的 close 方法会再把 isBatchingUpdates 改为 false
*/
// increment 方法为例
increment = () => {
// 进来先锁上
isBatchingUpdates = true
console.log('increment setState前的count', this.state.count)
this.setState({
count: this.state.count + 1
});
console.log('increment setState后的count', this.state.count)
// 执行完函数再放开
isBatchingUpdates = false
}
// 很明显, 在 isBatchingUpdates 的约束下, setState 只能是异步的
// reduce 方法为例
reduce = () => {
// 进来先锁上
isBatchingUpdates = true
setTimeout(() => {
console.log('reduce setState前的count', this.state.count)
this.setState({
count: this.state.count - 1
});
console.log('reduce setState后的count', this.state.count)
},0);
// 执行完函数再放开
isBatchingUpdates = false
}
/**
* 会发现, 咱们开头锁上的那个 isBatchingUpdates, 对 setTimeout 内部的执行逻辑完全没有约束力. 因为 isBatchingUpdates 是在同步代码中变化的, 而 setTimeout 的逻辑是异步执行的. 当 this.setState 调用真正发生的时候, isBatchingUpdates 早已经被重置为了 false, 这就使得当前场景下的 setState 具备了立刻发起同步更新的能力. 所以咱们前面说的没错——setState 并不是具备同步这种特性, 只是在特定的情境下, 它会从 React 的异步管控中'逃脱'掉.
* **/
总结
- 道理很简单,原理却很复杂。最后,我们再一次面对面回答一下标题提出的问题,对整个 setState 工作流做一个总结
- setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同:
# 在 React 钩子函数及合成事件中,它表现为异步
# 而在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为同步
- 这种差异,本质上是由 React 事务机制和批量更新机制的工作方式来决定的
Keep learning

浙公网安备 33010602011771号