之前面试被问到setState是同步的还是异步的,答得很糟糕,现在有时间了,结合网上查找的资料,跑一跑demo,来看看这个问题。
ps: 这里的同步异步是说执行setState后,获取的state是否就是更新后的state
看下官网上的内容
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
React可能对setState进行批量处理, 你拿到state不一定是更新后的state
来看下下面这个例子(React 15.6.2)
|
|
猜猜上面的输出是什么?
答案是:0 0 2 3
到chrome里运行看看,在setState处打上断点,查看一下调用栈
调用栈
setState
|
|
enqueueSetState
|
|
做的主要事情:
- 将
partialState存放_pendingStateQueue(待更新队列)里 - 执行
enqueueUpdate
enqueueUpdate
enqueueUpdate里直接调用ReactUpdates.enqueueUpdate
ReactUpdates.enqueueUpdate
先来看下执行到的enqueueUpdate里干了什么吧
这里batchingStrategy.isBatchingUpdates表示是否处于批量更新过程。
如果处于批量更新过程,将需要更新的组件添加到dirtyComponents里面
如果不是,则执行batchedUpdates操作。
/* * this.setState() * | * 存入 _pendingStateQueue * | * +----是否处于批量更新中----+ * | Y | N * | | * component 保存 batchedUpdates * dirtyComponents中 */
batchedUpdates
上一步的判断条件和调用方法都是在batchingStrategy上的,看一下ReactDefaultBatchingStrategy.js
|
|
看到这个isBatchingUpdates的初始值是false
batchedUpdates执行,会将isBatchingUpdates属性设为true
若isBatchingUpdates之前为false, 会执行transaction.perform来执行更新
那transaction是什么呢?我们先来了解一下
transaction
transaction.js的代码中,已经很形象地画出了perform执行的过程
* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+
transcation在创建时会注入wrappers,每个wrapper有initialize和close方法,当执行被perform包装的方法时,会依次执行每个wrapper的initialize方法,然后执行真正的方法,最后再依次执行wrapper的close方法。它提供了getTransactionWrappers这个抽象方法,可以去设置wrappers。
看一下使用方式:
看到 ReactDefaultBatchingStrategyTransaction 设置两个wrapper,先是FLUSH_BATCHED_UPDATES,然后是RESET_BATCHED_UPDATES
两个wrapper的initialize都没做事
FLUSH_BATCHED_UPDATES在close调用了flushBatchedUpdates做批量收集后的刷新工作,这里我把它想成state的真正更新。
RESET_BATCHED_UPDATES在close里重置isBatchingUpdates为false,表示当前批量处理流结束
demo中两种setState的结果
来看前2个setState调用栈
|
|
发现:
- setState 处在一个 transaction中
可以看到setState的执行栈里出现了batchedUpdates,而根据上面的代码我们知道,它会把isBatchingUpdates设成true,另外perform也在栈中,说明这个transaction没有结束。
因此setState执行到enqueueUpdate时,只是将对应的component暂存到dirtyComponents中,然后setState就执行完了。接着执行componentDidMount中的下一条语句console.log(this.state.count),而不是进入到这个transaction的close阶段的流程,所以这时state的值未发生变化。打印出的state还是原来的值。
setState步骤:
- 执行setState
- 经过层层调用(没有perform),将component加入dirtyComponents
- setState结束
再去看看后面2个的setState,因为放在了setTimeout里,而setTimeout 是异步的,它等之前的同步代码执行完后,才会调用。而同步代码执行完后isBatchingUpdates已经恢复成false了(transaction.perform在close时,把ReactDefaultBatchingStrategy.isBatchingUpdates被设成false)。
因此后2个setState进入batchedUpdates会调用一次transaction.perform处理(新建一个事务),包装的函数为enqueueUpdate,会把这次component放入dirtyComponents,perform执行完enqueueUpdate后,会去更新state,把ReactDefaultBatchingStrategy.isBatchingUpdates被设成false。perform调用结束后,setState也执行完了,接着往下执行console.log(this.state.count),这时打印出的结果就为更新的结果。
setState步骤:
- 执行setState
- 经过层层调用,执行到transaction.perform
- enqueueUpdate
- component加入dirtyComponents
- 更新state
- enqueueUpdate
- setState结束
打印出的结果
0 // {count: 1} 加入 dirtyComponents
0 // {count: 1} 加入 dirtyComponents
dirtyComponent被处理 state 变为 1
2 // {count: 2} 加入dirtyComponents,处理
3 // {count: 3} 加入dirtyComponents,处理
如何获取React更新后的state
看到以上,我们知道React对state的处理可能是“同步”,也可能是“异步”的
那有哪些方法是能获取到更新后的state?
官网推荐的方式setState(fn),传入的state会是上次的结果
123456789101112this.setState((state) => {console.log(state.count) // 0return {count: state.count + 1}})this.setState((state) => {console.log(state.count) // 1, 上个setState的state结果return {count: state.count + 1}})setState的回调,注意是批量更新后的结果,而不是一次setState后的结果,即它的回调是state批量更新后的才会调用的
12345678910this.setState({count: this.state.count + 1}, () => {console.log(this.state.count) // 2})this.setState({count: this.state.count + 2}, () => {console.log(this.state.count) // 2})上面我们看到的setTimeout的方式
什么时候会出现批量更新?
既然我们知道,batchedUpdates会开启一个transaction,进行批量更新,那只要在代码里搜索ReactUpdates.batchedUpdates,那找到的地方就可能是我们说的会批量更新的情况
部分搜索结果:
- ReactMount.js
挂载和卸载阶段的 _renderNewRootComponent和unmountComponentAtNode
这里涉及的生命周期有:componentWillMount 和componentDidMount
- ReactEventListener.js
dispathEvent,也就是React的事件,会走异步更新
如何批量更新
当你需要进行多次setState操作的时候,但是不在react的transaction时,你可以考虑用batchedUpdates来进行批量更新操作。React把batchedUpdates暴露了出来unstable_batchedUpdates
|
|
好了,到这你是否又对setState又有了进一步的了解呢?
还是和我一样心存疑问:
- state真正的更新又是如何处理的?
带着这些问题, 可以看下篇。