18 版本之前
经典面试题:setState 是同步还是异步
在 react 18 版本之前,在面试中经常会出现这个问题,那么答案又是什么样的呢?
- 在 React 合成事件中是异步的
- 在 hooks 中是异步的
- 其他情况皆是同步的,例如:原生事件、setTimeout、Promise 等
看看下面这段代码的执行结果,就知道所言非虚了
1class App extends React.Component {
2
3 state = {
4 count: 0
5 }
6
7 componentDidMount() {
8 this.setState({count: this.state.count + 1})
9 console.log(this.state.count);
10 this.setState({count: this.state.count + 1})
11 console.log(this.state.count);
12
13 setTimeout(() => {
14 this.setState({count: this.state.count + 1})
15 console.log(this.state.count);
16 this.setState({count: this.state.count + 1})
17 console.log(this.state.count);
18 });
19
20 }
21
22 render() {
23 return <h1>Count: {this.state.count}</h1>
24 }
25}
有经验的同学肯定都知道,最终的结果是: 0 0 2 3。
原因就是因为 componentDidMount 中的 setState 是批量更新,在整体逻辑没走完之前,不会进行更新。所以前两次打印结果都是 0,并且将两次更新合并成了一次。
而在 setTimeout 中,脱离了 React 的掌控,变成了同步更新,因为下方的 log 可以实时打印出即时的状态。
此时 React 的内部的处理逻辑我们可以写一段代码简单模拟一下:
- 先声明三个变量,用来记录数据
- isBatchUpdate: 判断是否批量更新的标志
- count: 状态
- queue: 存储状态的数组
- 声明一个 handleClick 方法,来模拟 React 合成事件
- 声明一个 setState 方法,来模拟 React 的 setState
1// 判断是否批量更新的标志
2let isBatchUpdate = false;
3// 状态
4let count = 0;
5// 存储最新状态的数组
6let queue = [];
7const setState = (state) => {
8 // 批量更新,则将状态暂存,否则直接更新
9 if (isBatchUpdate) {
10 queue.push(state);
11 } else {
12 count = state;
13 }
14}
15
16const handleClick = () => {
17 // 进入事件,先将 isBatchUpdate 设置为 true
18 isBatchUpdate = true
19
20 setState(count + 1)
21 console.log(count);
22 setState(count + 1)
23 console.log(count);
24 setTimeout(() => {
25 setState(count + 1)
26 console.log(count);
27 setState(count + 1)
28 console.log(count);
29 })
30
31 // 事件结束,将 isBatchUpdate 置为 false
32 isBatchUpdate = false;
33}
34
35handleClick();
36
37count = queue.pop();
38
39// 更新完成,重置状态数组 queue
40queue = [];
41
可以看到,上面这段代码的打印结果也是 0 0 2 3。
手动批量更新
上面提到,在原生事件以及 setTimeout 等情况下,setState 是同步的,那如果我们仍然希望这种情况下可以同步更新,该怎么办呢?
React
也提供了一种解决方案:从 react-dom
包中暴露了一个 API: unstable_batchedUpdates
那我们简单用一下看看效果:
1class App extends React.Component {
2
3 state = {
4 count: 0
5 }
6
7 componentDidMount() {
8 this.setState({count: this.state.count + 1})
9 console.log(this.state.count);
10 this.setState({count: this.state.count + 1})
11 console.log(this.state.count)
12
13 setTimeout(() => {
14 ReactDOM.unstable_batchedUpdates(() => {
15 this.setState({count: this.state.count + 1})
16 console.log(this.state.count)
17 this.setState({count: this.state.count + 1})
18 console.log(this.state.count)
19 })
20 })
21 }
22
23 render() {
24 return <h1>Count: {this.state.count}</h1>
25 }
26}
可以看到此时的打印结果为 0 0 1 1。
Ok,React 18 之前 setState 的更新方式就说到这里,那 React 18 里做了什么改动呢?
React 18 版本之后
上面提到了默认批量更新以及手动批量更新,那有些场景不满足了呀,觉得手动的还是不够智能,在很多情况下还得手动去调用
unstable_batchedUpdates 这个函数,用起来不爽。
别急,React 18 新版本就可以解决这些场景的痛点了!
直接上代码,看看 React 18 到底怎么用的:
1class App extends React.Component {
2
3 state = {
4 count: 0
5 }
6
7 componentDidMount() {
8 this.setState({count: this.state.count + 1})
9 console.log(this.state.count);
10 this.setState({count: this.state.count + 1})
11 console.log(this.state.count)
12
13 setTimeout(() => {
14 this.setState({count: this.state.count + 1})
15 console.log(this.state.count)
16 this.setState({count: this.state.count + 1})
17 console.log(this.state.count)
18 })
19 }
20
21 render() {
22 return <h1>Count: {this.state.count}</h1>
23 }
24}
25
26// 使用 react 18 新的并发模式写法进行 dom render
27ReactDOM.createRoot(document.getElementById('#root')!).render(<App />)
组件代码保持和第一版的一致,没有使用 unstable_batchedUpdates。
可以看到,此时的打印结果也是: 0 0 1 1
仅仅是使用了新的 API: ReactDOM.createRoot(root).render(jsx)。React 就能实现自动的批量更新了。感觉有点神奇。
我们依然写一段代码来模拟一下这个过程:
- 此时不需要 isBatchUpdate 来判断是否批量更新了,而是通过更新的优先级来进行判断
- 每次更新会进行优先级的判定,相同优先级的任务会被合并。
- 事件执行完毕,进行任务的执行和更新
1// 状态
2let count = 0;
3// 存储状态的数组
4let queue = [];
5const setState = (state) => {
6 const newState = {payload: state, priority: 0 }
7 // 判断当前优先级的任务集合是否存在,不存在则初始化,存在则存到对应由县级的任务集合中
8 if (queue[newState.priority]) {
9 queue[newState.priority].push(newState.payload)
10 } else {
11 queue[newState.priority] = [newState.payload]
12 }
13}
14
15const handleClick = () => {
16 setState(count + 1)
17 console.log(count);
18 setState(count + 1)
19 console.log(count);
20 setTimeout(() => {
21 setState(count + 1);
22 console.log(count);
23 setState(count + 1)
24 console.log(count);
25 })
26}
27
28handleClick();
29
30count = queue.pop().pop();
31
32setTimeout(() => {
33 count = queue.pop().pop();
34})
可以看到,上面这段代码的执行结果也是 0 0 1 1
上述模拟代码仅为了展示优先级批量更新,不代表任何 React 源码的逻辑和思想
自动批量更新的新特性就说到这里了。这里引入了三个问题
- Q: React 18 之后提供了
ReactDOM.createRoot
(root).render(jsx) 的 API,那之前ReactDOM.render
的 API 还支持吗?- 支持的,并且行为和之前版本是一致的。只有使用了
ReactDOM.createRoot
这种方式,才会启用新的并发模式。
- 支持的,并且行为和之前版本是一致的。只有使用了
- Q: React 全自动更新后,那如果我就是想拿到更新之后的数据怎么办呢?
- 类组件中可以使用
setState(state, callback)
的方式,在callback
中取到最新的值,函数组件可以使用useEffect
,将state
作为依赖。即可以拿到最新的值。
- 类组件中可以使用
- 文章中说到的优先级的概念是怎么回事呢?
- 这个涉及到 React 最新的调度以及更新的机制,优先级的概念以及其他优先级的任务如何创建,我们之后会一一展开来说。
- 目前的话,可以理解为 React 的更新机制进行了变化,不再依赖于批量更新的标志。而是根据任务优先级来进行更新:高优先级的任务先执行,低优先级的任务后执行。