上一篇文章 https://www.jianshu.com/p/272c7d36021a 分析了关于变量作用域以及闭包的问题。这篇文章来看看如何应用它来分析 react hook 中的闭包陷阱。
useEffect hook
useEffect 是 react 推出用于取代生命周期函数的 hook。它接收两个参数。第一个参数为一个回调函数,它在元素渲染后会执行。第二个参数为可选的数组,它控制回调函数的执行。有 3 种可能的情况:
- 省略:回调函数在每次渲染之后都会执行。
- 空数组时: 只有在第一次渲染后才执行,即只执行一次。
- 包含状态的数组: 只有渲染后且当这些状态有改变时执行。
在 react hook 中有个经典的闭包陷阱:
import "./styles.css";
import {useEffect, useState} from 'react'
export default function App() {
const [count, setCount] = useState(0)
useEffect(()=>{
setInterval(() => {
setCount(count + 1)
}, 1000);
}, [])
return (
<p>count={count}</p>
);
}
在代码中设置了一个定时函数,每隔 1s 将 count 的值加 1。并且将 useEffect 中的第二个参数设置为空数组。表示这个设置定时器的动作只进行一次。从理解来看,这种设置定时器的动作只需要进行一次即可。但是写过 react 的都知道,页面上 count 的值会一直保持 1,这显然不符合预期。
why
关于这个问题我在网上做了许多搜索,大部分只会提到这是一个过时闭包的问题,但没有人能将其讲清楚。实际上,正如我在上一篇文章中提到的,关于闭包问题,只需要记住这两点:
- 函数、控制块执行时会产生新的词法环境
- 当前词法环境找不到的变量会继续外外部词法环境寻找
对于问题提到的这个问题,我们首先需要知道当某个组件需要更新时,总是会调用这个组件对应的函数或者 render 函数。而每次函数调用总会生成一个新的词法环境。但是,对于 useEffect 中的回调函数,它始终捕获第一个函数调用时生成的词法环境中的 count(因为它只会执行一次,没有机会捕获第二次,第三次函数执行产生的词法环境中的 count),因此被捕获的 count 的值始终为 0,而 count + 1 的值也始终是 1。对于这种情况,如果希望出现预期的效果,就需要 useEffect 中的函数多次运行,不断的捕获新的词法环境中的 count。将 useEffect 改为:
useEffect(()=>{
setInterval(() => {
setCount(count + 1)
}, 1000);
}, [count])
这样才能保证能捕获到正确的 count。当然由于 setInterval 多次运行,所以这里还需要一个 clearInterval 的操作。
思考
import "./styles.css";
import {useEffect, useState} from 'react'
export default function App() {
const [count, setCount] = useState(0)
useEffect(()=>{
setInterval(() => {
console.info(count)
}, 1000)
}, [])
return (
<button onClick={setCount}>click me!</button>
);
}
如果多次点击按钮,控制台打印的 count 的值是否会发生改变呢?
同样:
(function (){
let count = 1;
setInterval(() => {
count += 1;
console.info(count)
}, 1000)
})()
控制台打印的值是否会发生改变? 如果你能正确的判断这两个例子的结果并且说明原因,那么 react 中的闭包陷阱应该不会再难到你。