上一篇文章我们知道了hooks的闭包陷阱是什么,它的成因和解决方法,并通过一个案例进行了演示。其实那种情况下的闭包陷阱的解决方案并不完美,所以我们在本文中对其进行改进。首先我们回顾一下什么是闭包陷阱:hooks的闭包陷阱是指在useEffect等hooks中使用了某个状态,但是没有加入到deps数组中,导致状态发生变化,但是执行的函数仍然参考与以前的状态。它的解决方法是正确设置deps数组,将使用过的状态放入deps数组中,这样每次状态变化时,都能执行最新的函数,并引用新的状态。同时清理最后一个定时器、事件监听器等。我们举了这样一个例子:import{useEffect,useState}from'react';functionDong(){const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{setCount(count+1);},500);},[]);useEffect(()=>{setInterval(()=>{console.log(count);},500);},[]);return
guang
;}exportdefaultDong;每次打印都是0:解决办法是把count设置为deps,加个清理函数:import{useEffect,useState}from'react';functionDong(){const[count,setCount]=useState(0);useEffect(()=>{consttimer=setInterval(()=>{setCount(count+1);},500);return()=>clearInterval(timer);},[count]);useEffect(()=>{consttimer=setInterval(()=>{console.log(count);},500);return()=>clearInterval(timer);},[count]);returnguang
;}exportdefaultDong;可以解决闭包陷阱:但是这种解决闭包陷阱的方法不太适合定时器。为什么?因为现在每次计数发生变化,定时器都会被重置,重新计算之前的计时,这样就会导致计时不准确。所以,这种在deps中加入依赖状态的方式可以解决闭包陷阱,而定时器却做不到这一点。那么还有什么可以解决闭包陷阱呢?使用参考闭包陷阱的原因是useEffect函数中引用了某个状态,形成了闭包,不直接引用也行吧?useRef是在memorizedState列表中放入一个对象,current保存一定的值。它的源码如下:初始化时在memorizedState上创建了一个对象,后面总是返回这个对象。这样通过useRef保存回调函数,然后在useEffect中从ref.current中取出函数再调用,避免了直接调用,不存在闭包陷阱的问题。就是这样:constfn=()=>{console.log(count);};constref=useRef(fn);useLayoutEffect(()=>{ref.current=fn;});useEffect(()=>{setInterval(()=>ref.current(),500);},[]);timer是在useEffect中执行的,deps设置为[],所以只会执行一次,回调函数使用ref.current,没有直接依赖于某个state,所以不会有闭包陷阱。使用useRef创建一个ref对象。初始值是一个打印计数的回调函数。每次渲染时,都会将其中的函数修改为新创建的函数。此函数中引用的计数是最新的。这里使用UseLayoutEffect而不是useEffect,因为useLayoutEffect是在render之前同步执行的,而useEffect是在render之后异步执行的,所以可以保证useLayoutEffect在useEffect之前被调用。该方法避免了在useEffect中直接引用state,从而避免了闭包问题。另外,修改count的地方,可以用setCount(count=>count+1)代替setCount(count+1),这样就避免了闭包问题:useEffect(()=>{setInterval(()=>{setCount(count=>count+1);},500);},[]);现在组件代码是这样的:import{useEffect,useLayoutEffect,useState,useRef}from'react';functionDong(){const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{setCount(count=>count+1);},500);},[]);constfn=()=>{console.log(count);};constref=useRef(fn);useLayoutEffect(()=>{ref.current=fn;});useEffect(()=>{setInterval(()=>ref.current(),500);},[]);returnguang
;}exportdefaultDong;测试:确实打印也正常,这是解决闭包陷阱的第二种方法,避免通过useRef直接引用state,从而避免闭包问题。这个逻辑使用了多个钩子,可以封装成自定义钩子:functionuseInterval(fn,time){constref=useRef(fn);useLayoutEffect(()=>{ref.current=fn;});useEffect(()=>{setInterval(()=>ref.current(),time);},[]);}然后组件代码可以简化:functionDong(){const[count,setCount]=useState(0);useInterval(()=>{setCount(count+1);},500);useInterval(()=>{console.log(count);},500);returnguang
;}这样我们就用useRef解决了闭包陷阱的问题。总结上一篇文章,我们通过在deps数组中添加依赖状态来解决闭包陷阱问题,这样每次状态变化时,都会执行一个新的函数并引用新的状态。这种方法不适用于定时器,因为定时器一旦复位重新计时,计时就会不准确。这就是为什么我们使用第二种方法来避免闭包陷阱:使用useRef。useRef之所以能解决闭包陷阱,是因为useEffect等hook不直接引用state,而是引用ref.current,这样后面只要修改ref中的值,这里取出来的就是最新的.然后我们将这个逻辑封装到一个自定义的钩子中,可以很容易地重复使用。解决hooks闭包陷阱的方法有两种:将依赖状态设置到deps数组中,加上清理函数。不是直接引用状态,而是将状态放入useRef创建的ref对象中,然后引用它。在处理定时器时,为了保证计时的准确性,最好使用useRef方法。在其他情况下,两者都很好。