场景
这是我刚开始用 react 的时候,在一个上传组件中遇到一个疑惑,如下伪代码:
beforeUpload(file) {
return new Promise<void>(async (resolve, reject) => {
try {
...
file.md5 = await md5File(file);
// 合并参数
const formData = new FormData();
...
setUploadUrl(uploadUrl);
setUploadData(formData);
resolve(file);
} catch (err) {
reject();
}
});
},
一开始以为,此处的 setter 是异步的,所以担心在后面操作的时候,获取不到最新的值。然而,在下面操作的时候,却发现拿到的是最新值,不免心生疑惑,难道 setState 不是异步的吗?于是开始搜搜搜~
探索
在函数式组件中,我们会这样定义状态:
const [count, setCount] = useState(0)
这时候,如果我们在同步函数或者在异步回调中调用 setCount 后,打印 count,都是旧值。往往这时候,就会给人错觉:setState 是异步的。
const [count, setCount] = useState(0);
// 直接调用
const handleUpdate = () => {
setCount(1);
console.log(count); // 0
};
// 放在setTimeout回调中
const handleAsyncUpdate = () => {
setTimeout(() => {
setCount(1);
console.log(count); // 0
});
};
其实,上述现象的原因不只是”异步“这么简单,原因还有以下两点:
- setState 里的逻辑其实是同步的,但是,调用 setState 时,react 会对这一系列的
setter做合并处理,异步更新该函数式组件对应的 hooks 链表里面的值,然后触发重渲染(re-renders),从这个角度上来说,setState确实是一个"异步"操作; - 函数式的
capture-value特性决定了console.log(count)语句打印的始终是一个只存在于当前帧的常量,所以无论 setState 是同步还是异步的,实际上这里都一定会打印出旧值。
那么,该怎么判断 state hooks 是同步还是”异步“呢?可以通过判断 state 更新时,dom 会不会同步更新,此处通过 render 次数观察:
import { useState } from "react";
const Example = () => {
console.log("do render >>");
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const update = () => {
console.log("update start");
setNum1(1);
console.log("update num1");
setNum2(2);
console.log("update num2");
};
const asyncUpdate = () => {
console.log("-----------------------------");
setTimeout(() => {
console.log("async update start");
setNum1(4);
console.log("async update num1:", num1);
setNum2(5);
console.log("async update num2:", num2);
});
};
return (
<div>
<p>num1 = {num1}</p>
<p>num2 = {num2}</p>
<button onClick={update}>update</button>
<button onClick={asyncUpdate}>asyncUpdate</button>
</div>
);
};
export default Example;
点击 update 按钮,可以观察到一下打印内容:
update start
update num1
update num2
do render >>
点击更新按钮(update),调用 setNum1 和 setNum2,根据打印内容可判断,这两个 setter 不是同步执行,若同步执行,在调用完 setState 后,会立刻 render,打印内容应该如下:
update start
do render >>
update num1
do render >>
update num2
接下来点击 asyncUpdate 按钮,输出以下打印内容
-----------------------------
async update start
do render >>
async update num1: 1
do render >>
async update num2: 2
可以看出此处是同步执行的。
结论
- 只要进入了
react的调度流程,那就是异步的;只要你没有进入react的调度流程,那就是同步的。 - 什么东西不会进入
react的调度流程?setTimeout、setInterval、直接在DOM上绑定原生事件、Promise 的回调等,这些都不会走React的调度流程。在这种情况下调用setState,那这次setState就是同步的。 否则就是异步的。 setState同步执行的情况下,DOM也会被同步更新,也就意味着如果多次setState,会导致多次更新,这是毫无意义并且浪费性能的。
回到开头,为什么我能够获取到最新值呢?在开头中,我使用了 await,而 await 后面的逻辑,相当于在 .then 回调里的逻辑,所以,结论2 就是答案啦~