import { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
return (
<div className="App">
<p>You clicked {count} times</p>
</div>
);
}
结果:
count变为1后不再更新
原因:
在函数(setTimeout / setInterval / fetch API / Promise)内,React 无法命中 batchUpdate,即无法批量更新,setState 是同步的而不是异步的。 那如果去掉 setInterval,直接多次 setState,效果会不会是一样的呢?
// setInterval等价于
useEffect(() => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// ....
}, []);
/**
* 输出: 1
**/
移除了 setInterval 函数,React 就会命中批量更新了,所以多个 setState 会合并成一个 setState,所以最终输出的是 1。
React 为了避免 dom 的频繁更新而造成的性能浪费(浏览器渲染线程和 js 线程是互斥的),会将多次 setState 的 update 合并成一次 update,不管是 class component 和 function component 都是这样的,不同之处在于,在 class component 中,this.setState 是合并 state,而 function component 是替换 state
state = {
data: "data",
data1: "data1",
};
this.setState({ data: "new data" });
console.log(state);
//{ data: 'new data',data1: 'data1' }
const [state, setState] = useState({ data: "data", data1: "data1" });
setState({ data: "new data" });
console.log(state);
//{ data: 'new data' }
在组件生命周期或 React 事件中, setState 是异步,在 setTimeout/setInterval 或者原生 dom 事件中, setState 是同步
但是有一种情况 react 控制不到,就是在 setTimeout、setInterval、Promise.then 这类函数的回调里调用 setState。
回到开头的那段代码,按照 react 批量更新的原理,在异步函数 setInterval 内,setState 应该是同步更新的,但实际发现 state 并没有改变,又是为什么呢?
其实这是闭包的问题,setInterval 回调函数形成了一个闭包,该闭包捕获了 count 变量的值 0,即使 count+1,但是 useEffect 依赖的是一个空数组,所以此时回调并不会执行(但是执行 setState 会也触发 re-render 啊,还是因为闭包的原因,这里的 count 每次都是 0,每次都+1,React 在 diff 的时候判断前后 state 值相等,不会触发 update DOM), 所以这个定时器只在组件初始化的时候执行了一次,并形成了闭包一直保存着,更新的过程并没有涉及到这个定时器函数(虽然定时器在默默地每隔一秒执行一次回调函数)。
如何才能每次都拿到最新的 state 呢?有两种方式:
/*
* 方法一:
* 将count加入useEffect的依赖,每次更新后清除当前过时的闭包
*/
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => {
clearInterval(id);
};
}, [count]);
return (
<div className="App">
<p>You clicked {count} times</p>
</div>
);
}
/*
* 方法二:
* 通过传递函数给setState
* 这样就更新了count状态,确保了将最新的状态值作为参数传递给了更新状态函数,过时的闭包问题就解决了
*/
function App_2() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count => count + 1);
}, 1000);
}, []);
return (
<div className="App">
<p>You clicked {count} times</p>
</div>
);
}
setState 机制:
-
会根据一个变量 isBatchingUpdates 判断是直接更新还是放到队列中等待
-
isBatchingUpdates 默认值是 false,在执行生命周期或 React 合成事件处理函数中会设置成 true,当执行完生命周期或事件处理函数后会变成 false,然后会一起更新状态和组件,所以整个过程看起来是异步的
-
在原生 DOM 事件中,react 不会调用批处理机制,所以 isBatchingUpdates 一直是 false,所以 setState 会直接更新
为什么 setState 要设置成异步?
本质上来讲 setState 是同步的, 之所以出现异步的假象是因为要进行状态合并或者说是批处理,需要等生命周期、事件处理器执行完毕, 再批量修改状态,这样做的目的:
-
保证 props 和 state 的一致性,实际开发过程中,状态会提升到父组件,和兄弟组件进行共享,如果 props 和 state 不一致很大概率会有问题
-
提高性能:避免多次更新 state 而渲染组件带来的性能损耗