前言
大家好,我是抹茶。 你是不是也曾好奇,为什么用useState定义的变量,在逻辑判断的时候出错,并没有按照他最新的值来。
本文围绕这个问题展开,争取可以对React对setState后的处理,形成大致的思路。
主要的更新过程(以函数组件为例)
流程图
在通过setState改变数据之后,其更新的流程如下图所示:
我们的问题是,useState定义的变量,在当前组件的执行上下文,不是我们期待的最新值,容易导致一些判断逻辑出错。
用demo验证语句的执行的顺序并观察state的变化
代码如:
import React, { FC,useState,useEffect } from 'react';
import { flushSync } from 'react-dom';
export type MePageProps = {};
const MePage: FC<MePageProps> = (props) => {
const [num, setNumber] = useState(0);
const addNum = function () {
setNumber(1);
console.log(num);
// 高优先级更新
flushSync(() => setNumber(2));
console.log(num);
setTimeout(() => {
setNumber(3);
console.log(num);
});
};
useEffect(() => {
console.log('监听num的变化,此时的num是:', num);
}, [num]);
return (
<>
数字当前的值{num}
<p>
<button onClick={addNum}>增加数字</button>
</p>
</>
);
};
export default MePage;
我们的数字,初始化的值为0。
当点击增加按钮的时候,console.log输出的快照显示为0,连用setTimoutout异步打印的值也为0。
但是用useEffect监听,是可以监听到num的变化。
观察结论
根据输出的结果,我们可以得出4点结论
-
console.log输出的是数据的快照
(更详细的底层设计见一个疑似bug,让我见识console.log的输出快照和事件数据的自动回收) -
useState定义的变量并不是实时同步更新的,state只有在下一次函数组件执行时才会更新到
当调用setNumber时,在本次函数的执行上下文,是获取不到最新的state值的。
原因在于:函数组件的更新就是函数的执行,在函数一次执行过程中,函数内部的所有变量重新声明,所以改变的state只有在下一次函数组件执行时才会被更新。在同一个函数执行上下文中,无论如何打印,都不会得到最新的state。 -
flushSync在提升优先级的同时,会把之前未更新的setState | useState一起合并了
在执行flushSync(() => setNumber(2));之前有个setNumber(1),但是useEffect并没有监听到这一变化,是因为其被合并了。 -
多次的
setNumber任务都会被一条一条语句的依次执行(哪怕是在flushSync之前的)
验证flushSync合并的执行细节代码如下:
const [num, setNumber] = useState(0);
const addNum = function () {
setNumber((n) => n + 1);
console.log(num);
// 高优先级更新
flushSync(() => setNumber((n) => n + 2));
console.log(num);
setTimeout(() => {
setNumber((n) => n + 3);
console.log(num);
});
};
useEffect(() => {
console.log('监听num的变化,此时的num是:', num);
}, [num]);
可以看到值是从0到6,所以三条赋值语句都执行了。
// 这三条赋值语句都被执行了
setNumber((n) => n + 1);
flushSync(() => setNumber((n) => n + 2));
setNumber((n) => n + 3);
但是useEffect的变化监听只输出了两次,所以flushSync合并之前的setState语句更准确是指,会减少useEffect的触发。
从上面的demo, 我们可以知道state是在下一次组件重新执行的时候更新值的,state最终的值是由多条赋值语句依次执行的结果敲定的。
异步更新,和批量处理的逻辑
更详细的流程如图:
实际上,在执行setState()之后,这个更新的函数并不是立即执行。React在内部维护了一个更新队列, 每一次setState()都会创建一个新的更新任务放到更新队列中。更新任务是有优先级的设计的,高优先级的会更快被执行。
而更新任务的执行时机是在浏览器空闲的时候。这个更具体的细节后面专门写章节介绍。
总结
本文围绕setState后的语句无法获取到最新state值展开,结合demo研究了setState实际上是异步更新的,且为了提高性能,React会先把更新任务放入一个更新队列,等浏览器空闲的时候再执行。
本文需要记住的要点如下:
- 不能用console.log打印useState定义的变量最新值,会出错,应该始终用useEffect监听state变化
- React中的更新任务是有优先级的
- flushSync能提高更新语句的优先级,如果在之前有对同个变量的setState语句,会被合并(useEffect的变化监听次数会减少,但是setState语句还是都会执行的)
更细节的,比如更新任务的优先级是用怎样方式实现?React是如何判断浏览器是否有空闲时间?更新任务执行的具体细节如何(从组件渲染到state变化,到浏览器重绘流程如何)将在后面的文章中介绍。