class 类组件中的 setState 和 hooks 函数组件中的 useState 的 状态修改函数 是一样的,有时同步,有时异步。
结论:
- setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
- setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
- setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和 setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
- 原生事件: 指的是绕过 React 通过 addEventListener 直接添加的事件处理函数
- 钩子函数: 各个生命周期函数
- 合成事件:由 React 引发的事件处理(比如通过 onClick 引发的事件处理)
React是怎样控制异步和同步的呢?
在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中延时更新,而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates 将 isBatchingUpdates 修改为true,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state。
总结:
只要你进入了 react 的调度流程,那就是异步的。只要你没有进入 react 的调度流程,那就是同步的。什么东西不会进入 react 的调度流程? setTimeout setInterval ,直接在 DOM 上绑定原生事件等。这些都不会走 React 的调度流程,你在这种情况下调用 setState ,那这次 setState 就是同步的。 否则就是异步的。
而 setState 同步执行的情况下, DOM 也会被同步更新,也就意味着如果你多次 setState ,会导致多次更新,这是毫无意义并且浪费性能的。
类组件测试:
import React, { PureComponent } from "react";
class setStateTest extends PureComponent {
constructor() {
super();
this.state = {
num: 0,
str: "111",
bool: false
}
};
componentDidMount() {
// 在 componentDidMount 生命周期里,连续修改 state ,只会触发一次render,state 被放在任务队列中批量更新,此时它们是异步的。
this.setState({
num: 3
})
console.log(this.state.num); // 0
this.setState({
str: "222"
})
this.setState({
num: 2
})
console.log(this.state.num); // 0
// 在定时器里会逐条触发 render ,说明此时是同步
setTimeout(() => {
this.setState({
num: 3
})
console.log(this.state.num); // 3
this.setState({
str: "222"
})
this.setState({
num: 2
})
console.log(this.state.num); // 2
}, 5000)
};
render() {
const { num, str, bool } = this.state;
console.log("触发render", num, str, bool);
return (
<>
<button onClick={() => { // 两个 setState 都执行了,但是只刷新了一次,所以它是异步的?
this.setState({
num: num + 1
})
this.setState({
bool: !bool
})
}}>setState同步异步测试</button>
</>
)
};
};
export default setStateTest;
函数式组件测试:
import React, { useEffect, useState } from "react";
export default function setStateTest() {
const [num, setNum] = useState(0);
const [str, setStr] = useState('11111');
useEffect(() => {
// 异步处理,会合并计算,值触发一次 刷新
setNum(2)
console.log(num); // 0 因为是异步处理,所以这里打印的是初始值 0
setNum(3)
console.log(num); // 0
setNum(4)
setStr("22222")
// 在定时器里会逐条触发 render ,说明此时是同步
// 为何 hooks 组件中 定时器 里面没有拿到更新后的值,这里是因为闭包的原因,只能拿到 初始 值 0
setTimeout(() => {
setNum(2)
console.log(num); // 0
setNum(3)
console.log(num); // 0
setNum(1)
setStr("33333")
}, 3000)
}, [])
return (
<>
<button onClick={() => {
setTimeout(() => { // 在定时器里会逐条触发 render ,说明此时是同步
setNum(2)
console.log(num); // 看点击的时候,当前的 num 是多少就是多少
setNum(3)
console.log(num); // 同上
setNum(1)
setStr("33333")
}, 0)
}}>{console.log("刷新了")}点击事件</button>
</>
)
};