React Hooks工具库 aHooks 解析之usePersistFn

前言

aHooks 是阿里巴巴开源的一个React Hooks库,其中有很多hooks实现得很巧妙,一起来看看吧,本文的主角是usePersistFn

这里是usePersistFn文档

usePersistFn

usePersistFn 解决了什么问题?

在某些场景中,你可能会需要用 useCallback 记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好,导致子组件重复 render。对于超级复杂的子组件,重新渲染会对性能造成影响。通过 usePersistFn,可以保证函数地址永远不会变化。

写个具体Demo描述下上述场景

function Child(props) {
  console.log("child render");
  return <button onClick={props.showCount}>showCount</button>;
}

const ChildMemo = memo(Child);

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

  const showCount = useCallback(() => {
    console.log("showCount");
  }, []);

  return (
    <div className="App">
      <button onClick={() => setCount((val) => val + 1)}>触发父组件渲染</button>
      <h2>count:{count}</h2>
      <ChildMemo showCount={showCount} />
    </div>
  );
}
复制代码

为了减少Child组件的渲染,我们使用memo配合useCallback,同时useCallback第二个参数传空数组,所以showCount只会被创建一次,这样当父组件重渲染时,ChildMemo不会重新渲染,达到了减少不必要渲染次数的目的。

但是当我们在showCount中想访问count变量时

const showCount = useCallback(() => {
+		console.log(count);
  }, []);
复制代码

我们会发现count的值不会更新,其实这是 react hooks 底层实现机制决定的,可以简单描述为闭包陷阱,具体就不展开讲了,解决办法是将count加到依赖项内。

const showCount = useCallback(() => {
+		console.log(count);
  }, [count]);
复制代码

问题解决了,此时可以访问到最新的count,但每次count变化时,都会重新创建showCount产生一个新的函数,从而导致memo失效。

针对这个问题,其实react官方给了个临时解决方案,那就是使用useRef

react.html.cn/docs/hooks-…

function Child(props) {
  console.log("child render");
  return <button onClick={props.showCount}>showCount</button>;
}

const ChildMemo = memo(Child);

function App() {
  const [count, setCount] = useState(0);
  const countRef = useRef();

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  const showCount = useCallback(() => {
    console.log(countRef.current);
  }, [countRef]);

  return (
    <div className="App">
      <button onClick={() => setCount((val) => val + 1)}>触发父组件渲染</button>

      <h2>count:{count}</h2>

      <ChildMemo showCount={showCount} />
    </div>
  );
}
复制代码

我们可以抽个自定义hooks,这也是react官方使用useRef的方案

function usePersistFn(fn, deps) {
  const fnRef = useRef();

  useEffect(() => {
    fnRef.current = fn;
  }, [fn, ...deps]);

  return useCallback(() => {
    return fnRef.current();
  }, [fnRef]);
}
复制代码

这样使用

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

  const showCountWithPersist = usePersistFn(() => {
    console.log(count);
  }, [count]);

  return (
    <div className="App">
      <button onClick={() => setCount((val) => val + 1)}>触发父组件渲染</button>

      <h2>count:{count}</h2>

      <ChildMemo showCount={showCountWithPersist} />
    </div>
  );
}
复制代码

本节代码 codesandbox.io/s/rough-daw…

但是每次使用都需要传递依赖项,比较麻烦,我们可以优化下,不需要传入依赖项。

我们传入依赖项的根本原因是因为希望依赖变化了,需要重新将 fn 赋值给fnRef.current

useEffect(() => {
    fnRef.current = fn;
  }, [fn, ...deps]);
复制代码

那我们只要不检查依赖是否变化,在每次函数执行时,无论依赖是否变化,我们都重新将 fn 赋值给fnRef.current,那就不需要使用者传递依赖了

function usePersistFn(fn) {
  const fnRef = useRef();
+  fnRef.current = fn; // 重点是这一行,去掉了useEffect

  return useCallback(() => {
    return fnRef.current();
  }, [fnRef]);
}
复制代码

本节代码 codesandbox.io/s/unruffled…

其实阿里开源的 react hooks 工具库 ahooks中的usePersistFn(github.com/alibaba/hoo…) 就是这种思路实现不需要传递依赖项的。

唯一不同的就是,它通过使用useRef代替了useCallback,但是最终达到的效果都是一样的,即都保证了返回的引用在usePersistFn被多次调用时都是相同的。

function usePersistFn(fn) {
  const fnRef = useRef(fn);
  fnRef.current = fn;

  const persistFn = useRef();
  if (!persistFn.current) {
    persistFn.current = function (...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return persistFn.crrent;
}
复制代码

首先创建persistFnref,然后第一次渲染时,!persistFn.current 会返回true,则会将匿名函数赋值给persistFn.current

至于return fnRef.current.apply(this, args); 使用apply只是为了保证this指向,匿名函数也可以换成箭头函数,就不需要apply,比如改成这样:

 persistFn.current =  (...args) => fnRef.current(args);
复制代码

来自React官方的建议

我们建议 在 context 中传递 dispatch ,而不是在 props(属性) 中单独回调。为了完整起见,并作为 escape hatch(逃生舱),此处仅提及以下方法。还要注意,此模式可能会在 并发模式 中导致问题。我们计划在将来提供更符合人们习惯的替代方案,但是目前最安全的解决方案是,如果某个值依赖于更改,则总是使回调无效。

主要是以下几点

  1. 当面临props需要在子/孙组件一层层传递时,请使用Context配合dispatch

但问题是现在直接使用Context会有大量无用渲染的问题,要想减少无用渲染,需要注意的点有点多,比如需要做到以下几点

  • 拆分不同粒度的Context

    const App = () => {
      // ...
      return (
        <ContextA.Provider value={valueA}>
          <ContextB.Provider value={valueB}>
            <ContextC.Provider value={valueC}>
              ...
            </ContextC.Provider>
          </ContextB.Provider>
        </ContextA.Provider>
      );
    };
    复制代码
  • 关注Context的顺序,让不变的放在在外层,多变的在内层。

总结

  • 谨慎使用useRef的方式来达到闭包穿透 的效果,在React18的并发模式(Concurrent Mode)可能会出现非预期的结果。
  • 不建议使用Context,若非要使用,使用时需要注意避免产生额外渲染行为,需要保持以下原则
    1. 拆分Context
    2. 关注Context的顺序,让不变的放在在外层,多变的在内层。
    3. 在当前React Context缺乏context selectors这种机制的情况下,建议使用状态管理库代替Context,毕竟大部分状态管理库都会带有selectors机制来优化性能。

Reference

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享