React useEffect的陷阱
0 条评论原文地址:https://zhuanlan.zhihu.com/p/84697185
今天就来讲一个useEffect这个Hook使用的一个小陷阱,看下面的代码,一个Counter,在窗口大小改变的时候,在console上输出当前count。
好久不写React的相关的东西,因为虽然这个技术作为工具还是在得到越来越多的应用,但是,React自Hooks和Suspense以来,也没有什么特别值得一说的新功能出来,所以,我也觉得真没有什么好写的:-)
回顾一下过去几个月,值得一提的,就是React Hooks正式推出之后暴露出来的一些小问题,这些小问题不是React的缺陷,而是开发者在面对Hooks这种新的思维方式时的水土不服。
今天就来讲一个useEffect这个Hook使用的一个小陷阱,看下面的代码,一个Counter,在窗口大小改变的时候,在console上输出当前count。
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
// 让resize事件触发handleResize
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const handleResize = () => {
// 把count输出
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当然会增长,比如我们现在让count增长为1,这时候你去改变浏览器窗口的大小,console上会输出什么呢?
你可能预期这样输出:
count is 1
事实上,输出是这样:
count is 0
怎么会这样?!
我先直接说这个问题怎么fix吧,关键在useEffect是用法上,正确的写法是这样:
useEffect(() => {
// 让resize事件触发handleResize
window.addEventListener('resize', handleResize)
return window.removeEventListener('resize', handleResize)
}, [count]) //看这一行!!! useEffect有第二个数组参数!!!
看了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一个全新的函数对象。
如果你觉得有点绕,我们详细复盘一下:
App第一次被渲染
给handleResize赋值了一个函数对象(我们姑且用XX-1代表),这个XX-1引用的count值是这一次App被渲染时的count值,值为0;
handleResize被useEffect挂到resize事件上,以后,当resize时间发生时,handleResize(应该说是XX-1)被调用;
App第二次被渲染
有一次给handleResize赋值了一个函数对象,代号YY-2,注意,这个YY-2和之前的XX-1不是同一个函数对象,XX-1依然引用的是值为0的count,但是YY-2引用的是值为1的count;
handleResize(也就是YY-2)没有被useEffect挂到resize时间上,换句话说,YY-2这个函数对象压根没有派上用场。
resize事件发生了
window上挂的resize事件处理,是第一次渲染时候创造的XX-1号handleResize,所以方位的count值为0
希望现在你明白了。
总结一下,请明白这几点:
- React Hooks只能用于函数组件,而每一次函数组件被渲染,都是一个全新的开始;
- 每一个全新的开始,所有的局部变量全都重来,全体失忆;
- 每一次全新的开始,只有Hooks函数(比如useEffect)具有上一次渲染的“记忆”;
对于上面说的问题,因为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,不要调用函数层次太多,代码应该一眼看清楚哪些函数会被useEffect调用。
- 本文链接:https://xuehuayu.cn/article/56926.html
- 版权声明:① 标为原创的文章为博主原创,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接。② 标为转载的文章来自网络,已标明出处,侵删。