奇怪的useMemo知识增加了

作为 「性能优化」 手段,一般用 useMemo 缓存函数组件中比较消耗性能的计算结果:

function App() {
  const memoizedValue = useMemo(
    () => computeExpensiveValue(a, b),
    [a, b]
  );
  // ...
}

只有在依赖项改变后才会重新计算新的 memoizedValue

你有没有想过,如果用 useMemo 缓存函数组件的返回值,会怎么样呢?

举个例子

我们有个全局 context —— AppContext

由于同学们偷懒,随着项目的迭代,新增的 context 都选择放在 AppContext 里,导致 AppContext 包含的内容越来越多。

现在我们有个 Tree 组件,他会渲染一个很耗性能的大组件 ExpensiveTree

function Tree() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme;

  return ;
}

该组件内部依赖 AppContext 中的 theme 状态。

由于 AppContext 中包含很多与 theme 无关的 state ,导致每次其他无关的 state 更新, Tree 都会重新 render ,进而 ExpensiveTree 组件也重新 render

现在这个优化任务交到了你手上,该怎么办呢?

优化ExpensiveTree

这时候, useMemo 就能派上用场:

function Tree() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme;

  return useMemo(() => {
    return ;
  }, [theme])
}

我们将返回的 ExpensiveTree 作为 useMemo 返回值, theme 作为依赖。

这样,即使 AppContext 改变导致 Tree 反复 renderExpensiveTree 也只会在 theme 改变后 render

原理解析

要理解这么做有效的原因,需要了解三点:

  1. useMemo 返回值是什么
  2. 函数组件的返回值是什么
  3. React 组件在什么时候 render

回答第一个问题: useMemo 会将第一个参数(函数)的返回值保存在组件对应 fiber 中,只有在依赖项(第二个参数)变化后才会重新调用第一个参数(函数)计算一个新值。

回答第二个问题:函数组件的返回值是 JSX 对象。

同一个函数组件调用多次,返回的是多个 「不同」JSX 对象(即使 props 未变,但 JSX 是新的引用)。

按照以上两个回答,我们可以得出结论:

以上 useMemo 用法实际上在函数组件对应的 fiber 中缓存了一个完整的 JSX 对象

第三个问题,函数组件需要同时满足如下条件才不会 render

  1. oldProps === newProps

前后两次更新 props 全等,注意是 「全等」

context

组件更新前后 fiber.type 未变化,比如 div 没有变为 p

  1. !includesSomeLane(renderLanes, updateLanes)

当前 fiber 上不存在更新,或者存在更新但优先级低。

更详细的解释,可以参考这篇文章: React组件到底什么时候render啊

当我们不使用 useMemo 包裹返回值,每次 Tree render 返回的都是全新的 JSX 对象。

所以对于 ExpensiveTreeoldProps !== newProps

再看2: ExpensiveTree 内部 context 没变,满足

再看3: ExpensiveTree 更新前后 type 都是 ExpensiveTree ,满足

再看4: ExpensiveTree 内没有状态更新,满足

所以,当我们使用 useMemo 包裹 ExpensiveTree 后,当 theme 不变,每次 Tree render 后返回的都是同一个 JSX 对象,满足第一条。

基于这个原因, ExpensiveTree 不会 render

总结

这篇文章提到的 useMemo 用法,并未在官网文档中体现,而是在 #15156 [1]中由 Dan 介绍。

相比 VueReact 更灵活,开发过程中需要开发者注意更多细节。要完全了解 React ,可能需要学习一些源码层面的知识。

参考资料

[1]

#15156: https://github.com/facebook/react/issues/15156#issuecomment-474590693