前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从根上理解 React Hooks 的闭包陷阱

从根上理解 React Hooks 的闭包陷阱

作者头像
神说要有光zxg
发布2022-06-06 08:40:11
2.5K1
发布2022-06-06 08:40:11
举报

现在开发 React 组件基本都是用 hooks 了,hooks 很方便,但一不注意也会遇到闭包陷阱的坑。

相信很多用过 hooks 的人都遇到过这个坑,今天我们来探究下 hooks 闭包陷阱的原因和怎么解决吧。

首先这样一段代码,大家觉得有问题没:

代码语言:javascript
复制
import { useEffect, useState } from 'react';

function Dong() {

    const [count,setCount] = useState(0);

    useEffect(() => {
        setInterval(() => {
            setCount(count + 1);
        }, 500);
    }, []);

    useEffect(() => {
        setInterval(() => {
            console.log(count);
        }, 500);
    }, []);

    return <div>guang</div>;
}

export default Dong;

用 useState 创建了个 count 状态,在一个 useEffect 里定时修改它,另一个 useEffect 里定时打印最新的 count 值。

我们跑一下:

打印的并不是我们预期的 0、1、2、3,而是 0、0、0、0,这是为什么呢?

这就是所谓的闭包陷阱。

首先,我们回顾下 hooks 的原理:hooks 就是在 fiber 节点上存放了 memorizedState 链表,每个 hook 都从对应的链表元素上存取自己的值。

比如上面 useState、useEffect、useEffect 的 3 个 hook 就对应了链表中的 3 个 memorizedState:

然后 hook 是存取各自的那个 memorizedState 来完成自己的逻辑。

hook 链表有创建和更新两个阶段,也就是 mount 和 update,第一次走 mount 创建链表,后面都走 update。

比如 useEffect 的实现:

特别要注意 deps 参数的处理,如果 deps 为 undefined 就被当作 null 来处理了。

那之后又怎么处理的呢?

会取出新传入的 deps 和之前存在 memorizedState 的 deps 做对比,如果没有变,就直接用之前传入的那个函数,否则才会用新的函数。

deps 对比的逻辑很容易看懂,如果是之前的 deps 是 null,那就返回 false 也就是不相等,否则遍历数组依次对比:

所以:

如果 useEffect 第二个参数传入 undefined 或者 null,那每次都会执行。

如果传入了一个空数组,只会执行一次。

否则会对比数组中的每个元素有没有改变,来决定是否执行。

这些我们应该比较熟了,但是现在从源码理清了。

同样,useMemo、useCallback 等也是同样的 deps 处理:

理清了 useEffect 等 hook 是在哪里存取数据的,怎么判断是否执行传入的函数的之后,再回来看下那个闭包陷阱问题。

我们是这样写的:

代码语言:javascript
复制
useEffect(() => {
    const timer = setInterval(() => {
        setCount(count + 1);
    }, 500);
}, []);

useEffect(() => {
    const timer = setInterval(() => {
        console.log(count);
    }, 500);
}, []);

deps 传入了空数组,所以只会执行一次。

对应的源码实现是这样的:

如果是需要执行的 effect 会打上 HasEffect 的标记,然后后面会执行:

因为 deps 数组是空数组,所以没有 HasEffect 的标记,就不会再执行。

我们知道了为什么只执行一次,那只执行一次有什么问题呢?定时器确实只需要设置一次呀?

定时器确实只需要设置一次没错,但是在定时器里用到了会变化的 state,这就有问题了:

deps 设置了空数组,那多次 render,只有第一次会执行传入的函数:

但是 state 是变化的呀,执行的那个函数却一直引用着最开始的 state。

怎么解决这个问题呢?

每次 state 变了重新创建定时器,用新的 state 变量不就行了:

也就是这样的:

代码语言:javascript
复制
import { useEffect, useState } from 'react';

function Dong() {

    const [count,setCount] = useState(0);

    useEffect(() => {
        setInterval(() => {
            setCount(count + 1);
        }, 500);
    }, [count]);

    useEffect(() => {
        setInterval(() => {
            console.log(count);
        }, 500);
    }, [count]);

    return <div>guang</div>;
}

export default Dong;

这样每次 count 变了就会执行引用了最新 count 的函数了:

现在确实不是全 0 了,但是这乱七八遭的打印是怎么回事?

那是因为现在确实是执行传入的 fn 来设置新定时器了,但是之前的那个没有清楚呀,需要加入一段清除逻辑:

代码语言:javascript
复制
import { useEffect, useState } from 'react';

function Dong() {

    const [count,setCount] = useState(0);

    useEffect(() => {
        const timer = setInterval(() => {
            setCount(count + 1);
        }, 500);
        return () => clearInterval(timer);
    }, [count]);

    useEffect(() => {
        const timer = setInterval(() => {
            console.log(count);
        }, 500);
        return () => clearInterval(timer);
    }, [count]);

    return <div>guang</div>;
}

export default Dong;

加上了 clearInterval,每次执行新的函数之前会把上次设置的定时器清掉。

再试一下:

现在就是符合我们预期的了,打印 0、1、2、3、4。

很多同学学了 useEffect 却不知道要返回一个清理函数,现在知道为啥了吧。就是为了再次执行的时候清掉上次设置的定时器、事件监听器等的。

这样我们就完美解决了 hook 闭包陷阱的问题。

总结

hooks 虽然方便,但是也存在闭包陷阱的问题。

我们过了一下 hooks 的实现原理:

在 fiber 节点的 memorizedState 属性存放一个链表,链表节点和 hook 一一对应,每个 hook 都在各自对应的节点上存取数据。

useEffect、useMomo、useCallback 等都有 deps 的参数,实现的时候会对比新旧两次的 deps,如果变了才会重新执行传入的函数。所以 undefined、null 每次都会执行,[] 只会执行一次,[state] 在 state 变了才会再次执行。

闭包陷阱产生的原因就是 useEffect 等 hook 里用到了某个 state,但是没有加到 deps 数组里,这样导致 state 变了却没有执行新传入的函数,依然引用的之前的 state。

闭包陷阱的解决也很简单,正确设置 deps 数组就可以了,这样每次用到的 state 变了就会执行新函数,引用新的 state。不过还要注意要清理下上次的定时器、事件监听器等。

要理清 hooks 闭包陷阱的原因是要理解 hook 的原理的,什么时候会执行新传入的函数,什么时候不会。

hooks 的原理确实也不难,就是在 memorizedState 链表上的各节点存取数据,完成各自的逻辑的,唯一需要注意的是 deps 数组引发的这个闭包陷阱问题。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-05-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 神光的编程秘籍 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档