Skip to content
Scroll to top↑

useMemorizedFn

将事件回调包裹在useCallback中通常会带来一个两难抉择:如果将useCallback的依赖数组置空以确保回调函数对象一直不变,可以避免不必要的重绘,但函数中访问的状态由于闭包捕获将不再更新;如果对React“诚实”,将函数所依赖的状态完整声明在useCallback数组中,则每次状态变更时我们都会得到一个新的回调方法,间接导致所有依赖此方法的组件重新渲染或者副作用重新执行。

示例中将TestBtn组件用React.memo包裹,这意味着除非TestBtnprops发生变化(浅比较不等),否则组件不会重新渲染:

tsx
const TestBtn: React.FC<{ onClick(): void, children: React.ReactNode }> = React.memo(({ onClick, children }) => {
  console.log(`rerender: ${children}`);

  return <button
    type="button"
    onClick={onClick}>
    {children}
  </button>
})

随后中创建三个“典型”回调,setCountNaive每次Demo渲染(重新执行组件方法)都会实例化一个,setCountNoDep不变,而setCountWithDep只会在count改变时重新实例化:

tsx
const Demo = () => {
  const [count, setCount] = useState(0);

  const setCountNaive = () => setCount(count + 1);
  const setCountNoDep = useCallback(() => {
    setCount(count + 1)
  }, []);
  const setCountWithDep = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return <div>
    <TestBtn onClick={setCountNaive}>Inc</TestBtn>
    <TestBtn onClick={setCountNoDep}>IncNoDep</TestBtn>
    <TestBtn onClick={setCountWithDep}>IncWithDep</TestBtn>
    <div>
      {count}
    </div>
  </div>
};

如前文所述,当我们点击按钮调用setCountNaivesetCountWithDep的时候,count会不断累加,触发Demo的渲染,间接导致三个TestBtn重新渲染,由于TestBtnReact.memo包裹,因此实际只有setCountNaivesetCountWithDep所属的TestBtn会重绘并在控制台打印:

rerender: Inc
rerender: IncWithDep

假如点击setCountNoDep,由于函数实例化时捕获的count0,因此它只会不断地将count设置为1。这不是我们想要的,有没有什么方法,能够既保证函数对象不变,又能保证函数执行时访问到的状态总是最新的呢?其实很简单,涉及到“不变”的时候通常会用到useRef或者其他绕过浅比较的机制,这里我们只需要将setCountNaiveref包裹,真正返回的是一个提取ref.current进行调用的包裹方法,再将这个方法用useCallback记住,ahooks 中的useMemoizedFn正是这样实现的:

ts
const useSmartCallback = <T, P>(fn: (...params: P[]) => T) => {
  const fnRef = useRef<typeof fn>(fn);

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

  return useCallback((...params: P[]) => fnRef.current(...params), []);
}
tsx
export default () => {
  const [count, setCount] = useState(0);

  const setCountNaive = () => setCount(count + 1);
  const setCountNoDep = useCallback(() => {
    setCount(count + 1)
  }, []);
  const setCountWithDep = useCallback(() => {
    setCount(count + 1);
  }, [count]);
  const setCountSmart = useSmartCallback(() => { 
    setCount(count + 1) 
  }) 

  return <div>
    <TestBtn onClick={setCountNaive}>Inc</TestBtn>
    <TestBtn onClick={setCountNoDep}>IncNoDep</TestBtn>
    <TestBtn onClick={setCountWithDep}>IncWithDep</TestBtn>
    <TestBtn onClick={setCountSmart}>IncSmart</TestBtn> // [!code ++]
    <div>
      {count}
    </div>
  </div>
};

下面例子在检测到重绘时将变更背景色,300ms之后才重置。点击各按钮并观察发生重绘的组件,IncSmart应当保持不变: