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

开发 架构
相信很多用过 hooks 的人都遇到过这个坑,今天我们来思考下 hooks 闭包陷阱的原因和怎么解决。

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

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

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

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 是在哪里存取数据的,怎么判断是否执行传入的函数的之后,再回来看下那个闭包陷阱问题。

我们是这样写的:

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 变量不就行了:

也就是这样的:

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 来设置新定时器了,但是之前的那个没有清楚呀,需要加入一段清除逻辑:

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-05 08:31:48

useRefuseEffecthook

2024-01-08 08:35:28

闭包陷阱ReactHooks

2021-02-24 07:40:38

React Hooks闭包

2016-10-27 19:26:47

Javascript闭包

2021-05-11 08:48:23

React Hooks前端

2016-09-18 20:53:16

JavaScript闭包前端

2022-08-21 09:41:42

ReactVue3前端

2022-10-24 08:08:27

闭包编译器

2011-03-02 12:33:00

JavaScript

2017-05-22 16:08:30

前端开发javascript闭包

2022-06-08 08:01:20

useEffect数组函数

2019-08-20 15:16:26

Reacthooks前端

2020-07-29 10:10:37

HTTP缓存前端

2010-07-26 11:27:58

Perl闭包

2023-11-06 08:00:00

ReactJavaScript开发

2022-05-06 16:18:00

Block和 C++OC 类lambda

2024-01-22 09:51:32

Swift闭包表达式尾随闭包

2020-04-27 09:40:13

Reacthooks前端

2021-03-07 17:17:07

Java内存闭包

2022-03-31 17:54:29

ReactHooks前端
点赞
收藏

51CTO技术栈公众号