关于前端:从-React-原理来看-ahooks-是怎么解决-React-的闭包问题的

2次阅读

共计 3771 个字符,预计需要花费 10 分钟才能阅读完成。

本文是深入浅出 ahooks 源码系列文章的第三篇,该系列已整顿成文档 - 地址。感觉还不错,给个 star 反对一下哈,Thanks。

本文来摸索一下 ahooks 是怎么解决 React 的闭包问题的?。

React 的闭包问题

先来看一个例子:

import React, {useState, useEffect} from "react";

export default () => {const [count, setCount] = useState(0);

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

  return (
    <div>
      count: {count}
      <br />
      <button onClick={() => setCount((val) => val + 1)}> 减少 1</button>
    </div>
  );
};

代码示例

当我点击按钮的时候,发现 setInterval 中打印进去的值并没有发生变化,始终都是 0。这就是 React 的闭包问题。

产生的起因

为了保护 Function Component 的 state,React 用链表的形式来存储 Function Component 外面的 hooks,并为每一个 hooks 创立了一个对象。

type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
};

这个对象的 memoizedState 属性就是用来存储组件上一次更新后的 statenext 指向下一个 hook 对象。 在组件更新的过程中,hooks 函数执行的程序是不变的,就能够依据这个链表拿到以后 hooks 对应的 Hook 对象,函数式组件就是这样领有了 state 的能力

同时制订了一系列的规定,比方不能将 hooks 写入到 if...else... 中。从而保障可能正确拿到相应 hook 的 state。

useEffect 接管了两个参数,一个回调函数和一个数组。数组外面就是 useEffect 的依赖,当为 [] 的时候, 回调函数只会在组件第一次渲染的时候执行一次 。如果有依赖其余项,react 会判断其依赖是否扭转,如果扭转了就会执行回调函数。

回到刚刚那个例子:

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

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

它第一次执行的时候,执行 useState,count 为 0。执行 useEffect,执行其回调中的逻辑,启动定时器,每隔 1s 输入 setInterval: 0

当我点击按钮使 count 减少 1 的时候,整个函数式组件从新渲染,这个时候前一个执行的链表曾经存在了。useState 将 Hook 对象 上保留的状态置为 1,那么此时 count 也为 1 了。然而执行 useEffect,其依赖项为空,不执行回调函数。然而之前的回调函数还是在的,它还是会每隔 1s 执行 console.log("setInterval:", count);,但这里的 count 是之前第一次执行时候的 count 值,因为在定时器的回调函数外面被援用了,造成了闭包始终被保留。

解决的办法

解决办法一:给 useEffect 设置依赖项,从新执行函数,设置新的定时器,拿到最新值。

// 解决办法一
useEffect(() => {if (timer.current) {clearInterval(timer.current);
  }
  timer.current = setInterval(() => {console.log("setInterval:", count);
  }, 1000);
}, [count]);

解决办法二:应用 useRef。
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。

useRef 创立的是一个一般 Javascript 对象,而且会在每次渲染时返回同一个 ref 对象 ,当咱们变动它的 current 属性的时候,对象的援用都是同一个,所以定时器中可能读到最新的值。

const lastCount = useRef(count);

// 解决办法二
useEffect(() => {setInterval(() => {console.log("setInterval:", lastCount.current);
  }, 1000);
}, []);

return (
  <div>
    count: {count}
    <br />
    <button
      onClick={() => {setCount((val) => val + 1);
        // +1
        lastCount.current += 1;
      }}
    >
      减少 1
    </button>
  </div>
);

useRef => useLatest

终于回到咱们 ahooks 主题,基于上述的第二种解决方案,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;

useEvent => useMemoizedFn

React 中另一个场景,是基于 useCallback 的。

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

const callbackFn = useCallback(() => {console.log(`Current count is ${count}`);
}, []);

以上不论,咱们的 count 的值变动成多少,执行 callbackFn 打印进去的 count 的值始终都是 0。这个是因为回调函数被 useCallback 缓存,造成闭包,从而造成闭包陷阱。

那咱们怎么解决这个问题呢?官网提出了 useEvent。它解决的问题:如何同时放弃函数援用不变与拜访到最新状态。应用它之后,下面的例子就变成了。

const callbackFn = useEvent(() => {console.log(`Current count is ${count}`);
});

在这里咱们不细看这个个性,实际上,在 ahooks 中曾经实现了相似的性能,那就是 useMemoizedFn。

useMemoizedFn 是长久化 function 的 Hook,实践上,能够应用 useMemoizedFn 齐全代替 useCallback。应用 useMemoizedFn,能够省略第二个参数 deps,同时保障函数地址永远不会变动。以上的问题,通过以下的形式就能轻松解决:

const memoizedFn = useMemoizedFn(() => {console.log(`Current count is ${count}`);
});

Demo 地址

咱们来看下它的源码,能够看到其还是通过 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,尽管解决了 class 组件的一些弊病,比方逻辑复用须要通过高阶组件层层嵌套等。然而也引入了一些问题,比方闭包问题。

这个是 React 的 Function Component State 治理导致的,有时候会让开发者产生纳闷。开发者能够通过增加依赖或者应用 useRef 的形式进行防止。

ahooks 也意识到了这个问题,通过 useLatest 保障获取到最新的值和 useMemoizedFn 长久化 function 的形式,防止相似的闭包陷阱。

值得一提的是 useMemoizedFn 是 ahooks 输入函数的规范,所有的输入函数都应用 useMemoizedFn 包一层。另外输出函数都应用 useRef 做一次记录,以保障在任何中央都能拜访到最新的函数。

参考

  • 从 react hooks“闭包陷阱”切入,浅谈 react hooks
  • React 官网团队出手,补齐原生 Hook 短板
正文完
 0