先记住这个答案
memo 用于减少 props 未改变时由父层渲染带来的重复工作,不会阻止组件收到自己读取的 Context 的新值。把 memo 放在 Provider 与消费者之间,或者直接包住消费者,都不能把 Context 更新隔离在外。优化时可以由外层组件读取 Context,只把昂贵子组件真正需要的数据作为 props 传进去,再让子组件使用 memo;需要的数据真的变化时,子组件仍应更新。
- 中间 memo 边界不截断 Context 的数据传递
- 消费者使用的 Context 变化仍需要更新
- 外层读取 Context,内层只接收必要 props
中间组件不更新,为什么深层仍能得到新值
假设 ThemeProvider 下有一个经过 memo 包装的布局,布局深处的按钮调用 useContext 读取主题。主题改变后,布局的普通 props 可以保持不变,但按钮依然需要得到新的主题。否则,只要在树的中间加入一个性能优化包装,深层数据就可能永久停在旧值,这会破坏 Context 的使用语义。
同样,给按钮本身加 memo 也不会冻结主题。分析这个过程时,不必把它描述成特定版本内部的订阅链表或逐层强制调用;面试中准确说明公开行为即可。若按钮被另一层同一个 Context 的 Provider 覆盖,它读取的是内层值,外层值的变化不能直接等同于这个按钮的 Context 输入变化。
把昂贵计算留在真正依赖它的组件里
一个报表卡片同时展示用户姓名和昂贵图形,而身份 Context 还包含经常改变的通知数量。可以在轻量容器里读取 Context,再把报表真正需要的用户 ID 或权限值传给经过 memo 包装的图形组件。通知数量变化时,容器负责读新值,图形组件则有机会复用输入未变的工作。
这种拆分要围绕真实依赖来做。如果图形颜色也取决于用户当前主题,却只传用户 ID,省略主题只是让缓存掩盖数据缺失。还要检查传入的配置对象和回调是否每次重新创建,否则子组件的 props 比较仍会发现变化。是否值得拆分,应由实际交互的耗时决定。
memo 不是渲染次数承诺
本地 state、消费的其它 Context 或变化的 props,都可能让同一个组件再次执行。memo 本身也是优化机制,不能让业务正确性依赖某段渲染逻辑永远只运行一次。组件渲染应保持纯粹,事件写入和外部订阅则放在对应的事件或同步流程中。
排查时可以记录触发动作和变化的输入,再结合 Profiler 观察耗时。开发环境的 Strict Mode 可能让日志次数与直觉不同,因此不要仅凭一次 console 输出就宣称优化成功。Context 拆分、缩小 Provider 的业务范围,也可能比增加多层 memo 更直接。
容易答错的地方
- 把 props 相同当作组件所有输入都相同
- Context 和本地状态同样影响渲染结果。只检查 props 就断言组件不该更新,会漏掉真实的数据变化;需要逐项检查它实际读取的输入。
- 自定义比较函数返回 true 就能屏蔽 Context
- 比较函数判断的是新旧 props 是否能产生相同输出,并没有提供忽略 Context 更新的选项。比较时遗漏函数等必要 prop,还可能制造旧闭包问题。
面试官还会怎么问?
React Compiler 会改变这一点吗?
不会改变消费者应获得 Context 新值的语义。编译器可以自动优化值、函数和组件相关工作,但不能把真正依赖的数据变化当成无关变化,也不能认为升级 React 就已经启用了编译器。
不读 Context 的中间组件一定不会重渲染吗?
不一定,它仍可能受父组件渲染、自己的状态或普通 props 影响。这里的结论只是 memo 不能阻断 Context 到消费者的数据更新,不能推导整条路径的所有组件都执行或都不执行。
只读取一个字段,memo 会自动按字段订阅吗?
内置 useContext 没有字段选择器语义。可以在读取之后向 memo 子组件传那个字段,或者按实际业务拆分 Context;前者减少的是内层工作,外层读取仍会随所消费的 Context 更新。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。