-
Notifications
You must be signed in to change notification settings - Fork 128
Description
复现问题
从一个最简单的demo开始,计算按钮的点击次数:
function App() {
const [count, setCount] = useState(0)
const update = () => {
setCount(count+1)
}
return (
<div className="App">
<span>COUNT: {count}</span> <button onClick={update}>+</button>
</div>
)
}这样肯定是没有问题的,点击按钮可以正常计数。如果这个时候我们将需求改一下,每秒钟要自动计数一次,那么代码就变成这样了:
function App() {
const [count, setCount] = useState(0)
const update = () => {
setCount(count+1)
}
useEffect(function autoCount() {// 取个名字方便讲解
const interval = setInterval(function repeat() { // 取个名字方便讲解
setCount(count+1) // 也可以写成update(),效果是一样的
}, 1000)
return () => clearInterval(interval)
}, [])
return (
<div className="App">
<span>COUNT: {count}</span> <button onClick={update}>+</button>
</div>
)
}这段看似正常的代码其实执行之后会发现有问题,自动计数只会统计到1,不会继续增加了。那么原因是什么呢? 是因为 autoUpdate 只执行了一次,这个函数中使用的 count 是第一次执行的时候App 闭包里面的值,也就是 0 ,所以每一次执行都会是 0+1=1 ,结果永远是1。
有些小伙伴这里可能不明白了, update 函数不也是闭包里面吗,为啥里面取到的 count 就能更新呢? 要回答这个问题就要理解 useEffect 的执行机制
useEffect执行过程
不需要去看 useEffect 的源码,只需要根据官网的描述我们就可以理解其执行逻辑:
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
useEffect 在每次 DOM 更新之后执行,准确的说,在上述例子中是 autoCount 在每次 DOM 更新后执行。而我们的第2个参数 [] 是执行条件,这样会导致只有第1次真的执行了 autoCount ,后面每次DOM更新都不会再执行。
我们的函数 APP 的执行过程是这样的:
- 执行
App函数
a. 获取count最新的值
b.autoCount函数进入effects队列
c. 返回VDOM - VDOM渲染为DOM
- 从
effects中取出autoCount函数,根据条件决定是否执行
用一张图来表示1次render的过程如下:

由于每一次 App 函数执行,都会读取新的 count 并创建新的 autoCount ,所以在新创建的 autoCount 函数内部读取的 count 值是最新的。然而我们定时更新不是定时执行最新的 autoCount ,而是在第1次的 count 内部不停执行 repeat 函数,因此读取的 count 值就永远是 1 。
而 update 函数执行没问题的原因是,每次更新都执行了 update ,所以总能取到最新的值。
理解的关键是要跳出class组件的思维。 在传统的class写法中,是同一个方法多次执行,每次执行后的结果不同是因为 this 上的数据不同,比如 render 方法。而在函数式组件中,组件内部声明的方法,如上例中 update , autoCount 等方法,每次执行App的时候都会声明一个新的,使用后就丢弃了。
解决方法
那么如果确实要做定时更新逻辑呢? 如何解决? 只要能读取最新的 count 值就可以,不要用闭包内的变量,而是直接用 setState 的回调函数语法获取最新值就可以了。
useEffect(function autoCount() {// 取个名字方便讲解
const interval = setInterval(function repeat() { // 取个名字方便讲解
setCount((count) =>count+1) // 也可以写成update(),效果是一样的
}, 1000)
return () => clearInterval(interval)
}, [])