当前位置: 首页 > 科技观察

从根本上理解ReactHooks的闭包陷阱

时间:2023-03-15 21:55:17 科技观察

现在React组件的开发基本都是使用hooks。Hooks很方便,但是稍不留神就会遇到闭包陷阱的坑。这个坑相信很多用过hook的人都遇到过。今天我们就来思考下hooks闭包陷阱产生的原因以及解决方法。首先,你觉得这段代码有没有问题: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;使用useState创建一个计数状态,在一个useEffect中定时修改,在另一个useEffect中定时打印最新的计数值。运行一下:打印出来的不是我们预期的0,1,2,3,而是0,0,0,0,这是为什么呢?这就是所谓的闭包陷阱。首先我们回顾一下hooks的原理:hooks在fiber节点上存储memorizedState链表,每个hook从对应的链表元素中访问自己的值。比如上面的useState、useEffect、useEffect这三个hook,对应链表中的三个memorizedStates:然后hooks访问各自的memorizedStates,完成自己的逻辑。hook链表有创建和更新两个阶段,即挂载和更新。第一次使用mount创建链表,之后会使用update。比如useEffect的实现:特别注意deps参数的处理。如果deps未定义,它将被视为null。之后怎么处理?新传入的deps会被取出来和之前存在的memorizedState中的deps进行比较。如果没有变化,则直接使用之前传入的函数,否则使用新的函数。deps比较的逻辑很好理解。如果前面的deps为null,则返回false,即不相等。否则会遍历数组依次比较:所以:如果useEffect的第二个参数传入undefined或者null,每次implement都会返回。如果传入一个空数组,则只会执行一次。否则,它将比较数组中的每个元素是否发生变化,以决定是否执行它。这些我们应该都不陌生,但是现在我们可以从源码中弄清楚它们。同样,useMemo、useCallback等也在同一个deps中进行处理:弄清楚useEffect等hooks访问数据的位置以及如何判断是否执行传递的函数后,再回到闭包陷阱问题。我们这样写:useEffect(()=>{consttimer=setInterval(()=>{setCount(count+1);},500);},[]);useEffect(()=>{consttimer=setInterval(()=>{console.log(count);},500);},[]);deps传递了一个空数组,所以只会执行一次。对应的源码实现如下:如果需要执行effect,会打上HasEffect的标记,后面再执行:因为deps数组是空数组,没有打上HasEffect的标记,就不会再次被执行。我们知道为什么只执行一次,那么只执行一次有什么问题呢?定时器只需要设置一次?定时器确实只需要设置一次,但改变的状态在定时器中使用。有一个问题:deps设置了一个空数组,所以对于多次渲染,第一次只会执行传递的函数:但是状态是变化的,执行的函数总是指初始状态。如何解决这个问题呢?每次状态改变时,重新创建计时器,只需使用新的状态变量:就是这样:import{useEffect,useState}from'react';functionDong(){const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{setCount(count+1);},500);},[count]);useEffect(()=>{setInterval(()=>{console.log(count);},500);},[count]);return
guang
;}exportdefaultDong;这样每次计数变化都会执行引用最新的计数函数:现在不全是0了,但是打印的乱七八糟是怎么回事?那是因为传入的fn确实是执行了设置一个新的定时器,但是之前的不清楚呀,我们需要添加一些清除逻辑:import{useEffect,useState}from'react';functionDong(){const[count,setCount]=useState(0);useEffect(()=>{consttimer=setInterval(())=>{setCount(count+1);},500);返回()=>clearInterval(计时器);},[数数]);useEffect(()=>{consttimer=setInterval(()=>{console.log(count);},500);返回()=>clearInterval(计时器);},[数数]);return
guang
;}exportdefaultDong;添加clearInterval,每次执行一个新的函数前,会重置上次的设置清除定时器再试:现在符合我们的预期,打印0,1,2,3,4。很多同学学了useEffect却没有'知道返回一个清理函数。现在我知道为什么了。就是再次执行时清除上次设置的定时器,事件监听器等。这样,我们就完美的解决了hookclosuretrap的问题。总结hooks虽然方便,但是也有闭包陷阱的问题。先看一下hooks的实现原理:fiber节点的memorizedState属性中存储了一个链表,链表节点与hooks一一对应,每个hooks访问其对应节点上的数据。useEffect、useMomo、useCallback等都有deps参数。实施时,他们会将新旧部门进行两次比较。如果它们发生变化,它们将重新执行传递的函数。所以undefined和null每次都会执行,[]只会执行一次,而[state]会在state改变的时候再执行一次。闭包陷阱的原因是useEffect等hook中使用了某个state,但是没有加入到deps数组中,导致state改变了但是没有执行新传过来的函数,仍然引用之前的状态。解决闭包陷阱的方法也很简单,正确设置deps数组即可,这样每次使用的状态发生变化时,都会执行一个新的函数,并引用新的状态。但是还要注意清理最后一个定时器,事件监听器等。弄清楚hooks关闭陷阱的原因就是理解hooks的原理,新传入的函数什么时候执行,什么时候不执行。hooks的原理确实不难。就是访问memorizedState链表上各个节点的数据,完成各自的逻辑。唯一要注意的是deps数组造成的闭包陷阱。