Deep In React(五)setState中的黑魔法
在React官方文档中有这么一句话React does not guarantee that the state changes are applied immediately。在我最开始使用React的时候,我只是简单的把这句话当做React这个框架的约束,但是随着使用的深入,setState这个函数也让我觉得越来越神秘。在这篇文章中,我将通过反思自己在使用react中遇到的关于setState的一些问题,深入react源码,分析setState这个函数。
以下代码全部基于 React15(React16 代码太复杂了看不懂哇- -)。
setState 不一定是同步的
在 React 官方文档中有这么一句话state-updates-may-be-asynchronous。
下面这两个很经典也是新人很容易糊涂的场景就是由上面这句模棱两可的话带来的。
class Demo extends Component {
state = {
count: 1,
};
onClickHandler = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // console.log 结果 1
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // console.log 结果 1
};
render() {
const { count } = this.state;
return <button onClick={this.onClickHandler}>{count}</button>;
}
}
class SetTimeoutDemo extends Component {
state = {
count: 1,
};
onClickSetTimeoutHandler = () => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // console.log 结果 2
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // console.log 结果 3
}, 0);
};
render() {
const { count } = this.state;
return <button onClick={this.onClickSetTimeoutHandler}>{count}</button>;
}
}
stackBlitz 的 demo 在这 Clicker 和 SetTimeoutClicker
上面的两个结果非常令人疑惑,在我刚刚接触 React 的时候,我并不是特别理解为什么这种写法会产生这样的差异,只是简单的相信这是 React 的一种 Magic。
setState 的内部实现
但是所有的 Magic 其实都有踪可循,经过一番调查,我大致理清楚了 setState 内部实现的调用关系。
setState 的内部调用栈如上图所示,略去一些细枝末节的代码之后,简化为如下的流程图。
在这个流程图中,有几个非常重要的地方需要关注
- 所有的
setState
时更新的 state 都以 partial state 的形式进入一个队列中,等待在 batchUpdate 中进行一次更新 - batch update 有一个
isBatchingUpdate
的锁,当正在进行 batching update 时,无法再次触发 batching update,当前 component 被 push 到dirtyComponent
数组中等待后续更新
以事务(transaction)的方式进行更新
在 batch update 中,React 使用事务机制进行更新,事务机制的运行原理如下
/**
* <pre>
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
* </pre>
*
*/
事务会在创造时注入多个 wrapper,每个 wrapper 是一个有着 initialize
和 close
两个函数的对象。当执行 perform(anyMethod)
的时候,调用顺序依次为
/**
wrapper1.initialize -> wrapper2.initialize -> anymethod -> wrapper1.close -> wrapper2.close
*/
batch update 中的事务
React 实现了两个 Wrapper 用作 batch update,在接入事务后,batch update 的流程如下
React 在一次事务中完成 batch update 锁的打开和关闭,来保证 batch update 的进行。
再回头看看我们之前的问题
回到我们之前的问题
在解释过 setState
内部的工作原理之后,其实对于上面这种奇怪的输出已经不难理解了。React 在事件触发时就已经处在了一个大的事务之中, isBatchingUpdate
被置成了 true
,随后的 setState 在调用时会进入 dirtyComponent
队列,在下一次 batch update 中进行更新。所以在下一次 batch update 之前, this.state
都不会得到更新。所以事实上调用结果如下。
//this.state.count = 1
this.setState({ count: this.state.count + 1 });
// 等于this.setState({count: 2})
this.setState({ count: this.state.count + 1 });
// 等于this.setState({count: 2})
而如果 setState
函数进行了 setTimeout
的包裹,由于EventLoop的特点,会保证 setState
一定是在前一条 message 之后,也就是上一次 batch update 完之后进行执行, isBatchingUpdate
为 false=,此时的 =setState
会直接触发一次完整的 batch update,保证 this.state
被同步更新。而下一次再进行 setTimeout
包裹的 setState
操作原理同上。
因为相同的原因,在组件生命周期中调用 setState
方法也会和事件触发类似, setState
并不会跟预期中的一样进行同步更新。
还有什么方式可以同步更新 state?
在上面的例子中,我们提到,在使用 React 封装的事件时会进入一个事务,使得 isBatchingUpdate
为 true
。 而当我们使用原生的事件机制时(比如 addEventListener
),由于缺少了 React 的封装,会使得 setState
直接触发 batch update 更新,从而同步更新 state。
class RawDemo extends Component {
constructor() {
super();
this.state = {
count: 1,
};
}
componentDidMount() {
document.querySelector('#foo').addEventListener('click', () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
});
}
render() {
return <button id="foo">{this.state.count}</button>;
}
}
总结
- React 中的 batch update 使用事务进行完成
- React 通过
isBatchingUpdate
来控制是否更新组件,当isBatchingUpdate
为true
时,组件会被推入dirtyComponent
数组中而不会即时更新 - 普通情况下之所以
setState
表现为非同步,原因是在 React 封装的事件绑定(或者在生命周期)中调用setState
处于一个大的事务中,isBatchingUpdate
已经被置为true
- 除了通过
setTimout
, 还可以通过原生的事件绑定机制来同步更新 state(并不推荐使用)。