5

React官方团队出手,补齐原生Hook短板

 2 years ago
source link: https://www.fly63.com/article/detial/11455
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

更新日期: 2022-05-06阅读量: 202标签: Hook分享

扫一扫分享

大家好,我卡颂。

我们知道,Hooks使用时存在所谓的「闭包陷阱」,考虑如下代码

function Chat() {
  const [text, setText] = useState('');

  const onClick = useCallback(() => {
    sendMessage(text);
  }, []);

  return <SendButton onClick={onClick} />;
}

我们期望点击后sendMessage能传递text的最新值。

然而实际上,由于回调函数被useCallback缓存,形成闭包,所以点击的效果始终是sendMessage('')。

这就是「闭包陷阱」。

以上代码的一种解决方式是「为useCallback增加依赖项」:

const onClick = useCallback(() => {
  sendMessage(text);
}, [text]);

但是这么做了后,每当依赖项(text)变化,useCallback会返回一个全新的onClick引用,这就失去了useCallback「缓存函数引用」的作用。

「闭包陷阱」的出现,加大了Hooks的上手门槛,也让开发者更容易写出有bug的代码。

现在,react官方团队要出手解决这个问题。

useEvent

解决方式是引入一个新的原生Hook —— useEvent。

他用于定义一个函数,这个函数有2个特性:

  1. 在组件多次render时保持引用一致。
  2. 函数内始终能获取到最新的props与state。

上面的例子使用useEvent改造后:

function Chat() {
  const [text, setText] = useState('');

  const onClick = useEvent(() => {
    sendMessage(text);
  });

  return <SendButton onClick={onClick} />;
}

在Chat组件多次render时,onClick始终指向同一个引用。

并且onClick触发时始终能获取到text的最新值。

之所以叫useEvent,是因为React团队认为这个Hook的主要应用场景是:「封装事件处理函数」。

useEvent的实现

useEvent的实现并不困难,代码类似如下:

function useEvent(handler) {
  const handlerRef = useRef(null);
  // 视图渲染完成后更新`handlerRef.current`指向
  useLayoutEffect(() => {
    handlerRef.current = handler;
  });
  // 用useCallback包裹,使得render时返回的函数引用一致
  return useCallback((...args) => {
    const fn = handlerRef.current;
    return fn(...args);
  }, []);
}

整体包括两部分:

返回一个没有依赖项的useCallback,使得每次render时函数的引用一致。

useCallback((...args) => {
  const fn = handlerRef.current;
  return fn(...args);
}, []);

在合适的时机更新handlerRef.current,使得实际执行的函数始终是最新的引用。

与开源Hooks的差异

很多开源Hooks库已经实现类似功能(比如ahooks中的useMemoizedFn)。

useEvent与这些开源实现的差异主要体现在:

useEvent定位于「处理事件回调函数」这一单一场景,而useMemoizedFn定位于「缓存各种函数」。

那么问题来了,既然功能类似,那useEvent为什么要限制自己的使用场景呢?

答案是:为了更稳定。

useEvent能否获取到最新的state与props取决于handlerRef.current更新的时机。

在上面模拟实现中,useEvent更新handlerRef.current的逻辑放在useLayoutEffect回调中进行。

这就保证了handlerRef.current始终在「视图完成渲染」后再更新:

useLayoutEffect(() => {
  handlerRef.current = handler;
});

而「事件回调」触发的时机显然在「视图完成渲染」之后,所以能够稳定获取到最新的state与props。

注:源码内的实际更新时机会更早些,但不影响这里的结论。

再来看看ahooks中的useMemoizedFn,fnRef.current的更新时机是「useMemoizedFn执行时」(即「组件render时」):

function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn);
  // 更新fnRef.current
  fnRef.current = useMemo(() => fn, [fn]);
  // ...省略代码
}

当React18启用「并发更新」后,组件render的次数、时机并不确定。

所以useMemoizedFn中fnRef.current的更新时机也是不确定的。

这就增加了在「并发更新」下使用时潜在的风险。

可以说,useEvent通过限制handlerRef.current更新时机,进而限制应用场景,最终达到稳定的目的。

useEvent当前还处于RFC(Request For Comments)[1]阶段。

很多热心的开发者对这个Hook的命名提出了建议,比如:useStableCallback:

6274b825c3a13.jpg

又比如:useLatestClosure:

6274b82a67d60.jpg

从这些命名看,他们显然扩大了useEvent的应用场景。

经过本文的分析我们知道,「扩大应用场景」意味着「增加开发者使用时出错的风险」。

来源: 魔术师卡颂

链接: https://www.fly63.com/article/detial/11455


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK