React Hooks 中的闭包问题
Posted 前端More
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Hooks 中的闭包问题相关的知识,希望对你有一定的参考价值。
React Hooks 中的闭包问题
React 自从引入 hooks,虽然解决了类组件的一些弊端,但是也引入了一些问题,比如闭包问题。
闭包问题
先看一个例子
import React, useState, useEffect from "react";
export default () =>
const [count, setCount] = useState(0);
useEffect(() =>
setInterval(() =>
console.log("当前值:", count);
, 1000);
, []);
return (
<>
count: count
<br />
<button onClick=() => setCount((val) => val + 1)>增加 1</button>
</>
);
;
当增加按钮的时,发现当前值打印始终都是 0,没有发生变化。这就是 React 的闭包问题。
闭包产生的原因
为了维护函数组件的state,React 用链表
的方式来存储函数组件里面的 hooks,并为每一个 hooks 创建了一个对象。hooks 函数执行的顺序是不变的,就可以根据这个链表拿到当前 hooks 对应的 Hook 对象,函数式组件就是这样拥有了state的能力
//hooks对象
type Hook =
memoizedState: any,//存储上一次组件更新后的state
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,//指向下一个 hook 对象
;
回到上面的示例
const [count, setCount] = useState(0);
useEffect(() =>
setInterval(() =>
console.log("当前值:", count);
, 1000);
, []);
代码第一次执行:执行 useState,count 为 0。执行 useEffect,执行其回调中的逻辑,启动定时器,每隔 1s 输出 当前值: 0
。
点击增加按钮:当state更新时, 链表从头开始重新渲染,,useState 将 Hook 对象 上保存的状态置为 1, 那么此时 count 也为 1 了。执行 useEffect,其依赖项为空,不执行回调函数。但是之前的回调函数还是在的,它还是会每隔 1s 打印count,但这里的 count 还是之前第一次执行时候的 count 值,该count值定时器的回调函数里面被引用了,就形成了闭包一直被保存。
闭包产生的原因:就是当前hooks中没有获取到最新的state
如何解决
方法一: 清除重建
给 useEffect 设置依赖项,重新执行函数,设置新的定时器,拿到最新值。
import React, useState, useEffect from "react";
export default () =>
const [count, setCount] = useState(0);
//声明一个定时器
const timer = useRef(null);
useEffect(() =>
//定时器期间,有新操作时,清空旧定时器,重设新定时器
//定时器存在 则清空
if (timer.current)
clearInterval(timer.current);
//重新设定时器,保证打印的永远是最新的值
timer.current = setInterval(() =>
console.log("当前值:", count);
, 1000);
, [count]);
return (
<>
count: count
<br />
<button onClick=() => setCount((val) => val + 1)>增加 1</button>
</>
);
;
方法二: 使用 useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。
useRef 创建的是一个普通JS对象,而且会在每次渲染时返回同一个 ref 对象,当我们变化它的 current 属性的时候,操作的都是同一个对象,所以定时器中能够读到最新的值。
import React, useState, useEffect from "react";
export default () =>
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() =>
setInterval(() =>
console.log("当前值:", latestCount.current);
, 1000);
, []);
return (
<div>
count: count
<br />
<button
onClick=() =>
setCount((val) => val + 1);
latestCount.current += 1;
>
增加 1
</button>
</div>
);
方法三: ahooks中的useLatest
基于上述的第二种解决方案,useLatest 这个 hook 随之诞生。它返回当前最新值的 Hook,可以避免闭包问题。实现原理很简单,就是使用 useRef 包一层:
import useRef from 'react';
// 通过 useRef,保持每次获取到的都是最新的值
function useLatest<T>(value: T)
const ref = useRef(value);
ref.current = value;
return ref;
export default useLatest;
代码实现
import React, useState, useEffect from "react";
import useLatest from 'ahooks';
export default () =>
const [count, setCount] = useState(0);
const latestCountRef = useLatest(count);
useEffect(() =>
const interval = setInterval(() =>
console.log("当前值:", latestCountRef.current);
, 1000);
return () => clearInterval(interval);
, []);
return (
<>
count: count
<br />
<button onClick=() => setCount((val) => val + 1)>增加 1</button>
</>
);
;
方法四:ahooks中的 useMemoizedFn
React 中另一个闭包场景,是基于 useCallback 的。
const [count, setCount] = useState(0);
const callbackFn = useCallback(() =>
console.log(`Current count is $count`);
, []);
以上不管,我们的 count 的值变化成多少,执行 callbackFn 打印出来的 count 的值始终都是 0。这个是因为回调函数被 useCallback 缓存,形成闭包,从而形成闭包陷阱。
那我们怎么解决这个问题呢?官方提出了 useEvent-- 相当于useCallback的升级版,但该题案没有通过,后续官方还会给出新的解决方法。useEvent保持函数引用不变与访问到最新状态。使用它之后,上面的例子就变成了。
const callbackFn = useEvent(() =>
console.log(`Current count is $count`);
);
在 ahooks 中已经实现了类似的功能,useMemoizedFn
是持久化 function 的 Hook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。使用 useMemoizedFn,可以省略第二个参数 deps,同时保证函数地址永远不会变化。
const memoizedFn = useMemoizedFn(() =>
console.log(`Current count is $count`);
);
我们来看下它的源码,可以看到其还是通过 useRef 保持 function 引用地址不变,并且每次执行都可以拿到最新的 state 值。
function useMemoizedFn<T extends noop>(fn: T)
// 通过 useRef 保持其引用地址不变,并且值能够保持值最新
const fnRef = useRef<T>(fn);
fnRef.current = useMemo(() => fn, [fn]);
// 通过 useRef 保持其引用地址不变,并且值能够保持值最新
const memoizedFn = useRef<PickFunction<T>>();
if (!memoizedFn.current)
// 返回的持久化函数,调用该函数的时候,调用原始的函数
memoizedFn.current = function (this, ...args)
return fnRef.current.apply(this, args);
;
return memoizedFn.current as T;
参考
以上是关于React Hooks 中的闭包问题的主要内容,如果未能解决你的问题,请参考以下文章