今天讲一个 使用 useEffect Hooks 的时候遇到的一个小陷阱,看下面的代码。
一个Counter,在窗口大小改变的时候,输出当前count:
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const handleResize = () => {
console.log(`count is ${count}`)
}
return (
<div className="App">
<button onClick={() => setCount(count + 1)}> + </button>
<h1>{count}</h1>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Counter:
现在我们如果点击 + 按钮,下面的数字0会加1 .
这时候你去改变浏览器窗口的大小,console上会输出什么呢?
你可能觉得是1 :
count is 1
但是事实上,输出是这样:
count is 0
怎么会这样呢?
我先直接说这个问题怎么修复吧。
关键在useEffect是用法上,正确的写法是这样:
useEffect(() => {、
window.addEventListener('resize', handleResize)
return window.removeEventListener('resize', handleResize)
}, [count]) // 加 Count
看了fix之后你也许就知道这是怎么回事了。
useEffect的第二个参数可选,如果用上的话,这个参数必须是一个数组。
useEffect 在每次被调用的时候,都会“记住”这个数组参数,当下一次被调用的时候,会逐个比较数组中的元素,看是否和上一次调用的数组元素一模一样,如果一模一样,第一个参数(那个函数参数)也就不用被调用了,如果不一样,就调用那个第一个参数。
当我们代码中的App组件第一次被渲染的时候,useEffect百分之百会调用第一个函数参数,这时候count变量是0,但是,当我们点+按钮让Counter增长为1,这时候App被重新渲染,但是因为useEffect第一个参数总是一个空数组,所以不会重新做addEventListener的工作。
你可能又会问:就算useEffect不重新执行第一个函数参数,也不应该有什么问题啊,handleResize函数利用闭包(clousre)功能访问App中的count变量,那也应该是使用更新为1的count啊!
虽然闭包的确可以访问外围的变量,但是,此handleResize非彼handleResize。
第一次渲染时的handleResize和第二次渲染时的handleResize,虽然源自同一段代码,但是在运行时却是两个不同的函数对象。这并不难理解,handleResize是一个局部变量,每次App被执行时,handleResize都会被重行赋值,所以每一次App被渲染时,都会给handleResize一个全新的函数对象。
复盘一下:
希望现在你明白了。
总结一下,要明白这几点:
对于上面说的问题,因为count每次渲染都会改变,而且我们想要 useEffect 总会用上count的值,所以,就要把count放在useEffect的第二个数组参数里面。
如果useEffect第一个函数参数直接或者间接用上某个变量,就请把这个变量放在useEffect的第二个参数里。
如果根本不用useEffect的第二个参数呢?
也行,但是,这样每次渲染都会执行useEffect的第一个参数,这……在某些场景下有一点性能浪费。
其实要做到上面的规矩,也没那么难,不过在实际操作的时候,的确让人容易失误,你看,在上面的例子中,useEffect并没有直接使用count,只不过使用了handleResize,handleResize虽然直接使用了count,但是它作为一个独立函数并不知道(或者说也不该知道)自己会被useEffect用到,这……让人防不胜防啊!
这只有一层简介调用,假设useEffect调用了函数X,函数X调用了Y,Y调用了Z。
调用N层之后再调用 handleResize,真的不容易看出useEffect需要加上对count的依赖。
所以,使用useEffect的时候,不要调用函数层次太多,代码应该一眼看清楚哪些函数会被useEffect调用。
最后, eslint-plugin-react-hooks 插件可以给出依赖提示, 一定程度上能避免类似的问题。