DEV Community

Lydia_Yuan
Lydia_Yuan

Posted on

useMemo / useCallback 将来可能的隐患以及可行的对策

起因

这两个 API 可能不够 stable,未来可能导致代码库里用到的地方需要进行相关改动
具体可以看官方文档中有这么一段话

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

总结一下 React core team 成员对这个问题的回复

如果未来真的上这个 feature

  • 会提供额外的 API 来 mark 当前组件是否允许进行内存回收
  • 只会在「能够合理破坏 memoization」的地方进行回收,比如「一个 tab 隐藏了几分钟,可以释放掉仅仅被 memoization 占用的额外内存」

社区已经有的替代方案

useMemoOne
基本是用 useRef 重新实现了一遍 useMemouseCallback

useCreation
ahooks 的一个 useMemo 的替代品 或者说是根据 deps 变化的 useRef

目前 useRefuseMemo useCallback 具体实现原理

useRef

    function useRef<T>(initialValue: T): {|current: T|} {
      currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
      workInProgressHook = createWorkInProgressHook();
      const previousRef = workInProgressHook.memoizedState;
      if (previousRef === null) {
        const ref = {current: initialValue};
        if (__DEV__) {
          Object.seal(ref);
        }
        workInProgressHook.memoizedState = ref;
        return ref;
      } else {
        return previousRef;
      }
    }
Enter fullscreen mode Exit fullscreen mode

useMemo

    function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T {
      currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
      workInProgressHook = createWorkInProgressHook();

      const nextDeps = deps === undefined ? null : deps;

      if (workInProgressHook !== null) {
        const prevState = workInProgressHook.memoizedState;
        if (prevState !== null) {
          if (nextDeps !== null) {
            const prevDeps = prevState[1];
            if (areHookInputsEqual(nextDeps, prevDeps)) {
              return prevState[0];
            }
          }
        }
      }

      if (__DEV__) {
        isInHookUserCodeInDev = true;
      }
      const nextValue = nextCreate();
      if (__DEV__) {
        isInHookUserCodeInDev = false;
      }
      workInProgressHook.memoizedState = [nextValue, nextDeps];
      return nextValue;
    }
Enter fullscreen mode Exit fullscreen mode

useCallback

    export function useCallback<T>(
      callback: T,
      deps: Array<mixed> | void | null,
    ): T {
      return useMemo(() => callback, deps);
    }
Enter fullscreen mode Exit fullscreen mode

可见 useCallback 基本可以认为是为了传入函数更方便(少写一层 () => fn)的 useMemo,因此也会被这个问题牵扯

相关资料

  1. 关于这个问题的详细 issue 讨论: https://github.com/facebook/react/issues/15278
  2. 比较流行的一个 useMemo 使用 useRef 重写的内存安全的替代品 useMemoOne: https://github.com/alexreardon/use-memo-one
  3. React core team 成员在 Reddit 上的回复: https://www.reddit.com/r/reactjs/comments/mib8mr/usememo_docs_dont_reflect_how_it_is_used_in/gt6ouh8?utm_source=share&utm_medium=web2x&context=3
  4. 各种 hooks 的具体实现: https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/server/ReactPartialRendererHooks.js

PS MST observer 用的是 React 的 top-level API React.memo 不是 useMemo,所以暂时不用担心

Discussion (0)