useMemo / useCallback 将来可能的隐患以及可行的对策
Published: 2021-11-25 06:15:37

## 起因

这两个 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 成员对这个问题的回复](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)

如果未来真的上这个 feature

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


## 社区已经有的替代方案

[**useMemoOne**](https://github.com/alexreardon/use-memo-one/blob/master/src/index.js)
基本是用 `useRef` 重新实现了一遍 `useMemo` 和 `useCallback`

[**useCreation**](https://ahooks.js.org/hooks/advanced/use-creation)
ahooks 的一个 `useMemo` 的替代品 或者说是根据 deps 变化的 `useRef` 

## 目前 `useRef` 和 `useMemo` `useCallback` 具体实现原理

[**useRef**](https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/server/ReactPartialRendererHooks.js#L372)

```typescript
    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;
      }
    }
```

[**useMemo**](https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/server/ReactPartialRendererHooks.js#L343)

```typescript
    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;
    }
```

[**useCallback**](https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/server/ReactPartialRendererHooks.js#L446)

```typescript
    export function useCallback<T>(
      callback: T,
      deps: Array<mixed> | void | null,
    ): T {
      return useMemo(() => callback, deps);
    }
```

可见 `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](https://github.com/mobxjs/mobx/blob/e469416cf4b882f18f48810c231cef2d4965ef56/packages/mobx-react-lite/src/observer.ts#L70) 用的是 React 的 top-level API [React.memo](https://reactjs.org/docs/react-api.html) 不是 `useMemo`，所以暂时不用担心