先记住这个答案
Effect 逐项用 Object.is 比较依赖。组件体内创建的配置对象或函数,即使内容看起来相同,也通常不是上次的引用,因此可能使 Effect 在每次提交后重新同步。先看这个对象或函数能否移到 Effect 内部,再依赖实际使用的原始输入;真正静态的内容也可以移到组件外。确实需要在多个地方共享引用时再评估 useMemo 或 useCallback,不能为减少重跑而遗漏真实依赖。
- 字段相同不等于对象或函数引用相同
- 优先调整构造位置,减少无意义的依赖身份
- 缓存依赖仍需完整,缓存不是资源生命周期保证
一次输入框更新为何让外部连接重建
假设页面同时有搜索输入和文档预览连接,组件体每次构造包含 documentId 的 options 对象,Effect 依赖 options。用户只是输入搜索词,documentId 没变,但 options 已经是新对象,因此清理与重连仍可能发生。把问题归咎于 React 无故重跑,会漏掉这个明确的引用变化。
若 options 只被该 Effect 使用,可以在 Effect 内根据 documentId 创建它,让依赖直接表达 documentId。这样每一轮同步仍有独立配置对象,但构造发生在真正需要建立同步的时候,无关输入不再仅因包装引用不同就要求重连。
辅助函数也要区分定义位置与实际输入
组件中定义 buildOptions 函数,再让 Effect 依赖它,也可能出现同样问题。函数每次重新创建,即使函数体文本完全一样,引用仍不同。如果这个函数只为该 Effect 服务,可以将它放入 Effect;若只是纯粹根据参数生成配置,也可在组件外定义并显式传入必要参数。
不能把读取当前 props 的函数直接搬到模块顶层,然后依赖一个共享可变变量提供最新值,这会混淆组件实例与请求范围。移出组件的代码应真正独立于该实例的渲染输入,或把需要的值作为参数传递,而不是用全局变量绕过依赖工具。
什么时候缓存比移动构造更合适
一个配置对象同时传给第三方适配层和多个子组件,确实需要稳定共享身份时,可以评估 useMemo;回调被多处使用时也可评估 useCallback。缓存依赖必须涵盖读取的响应式输入,参数变化时产生新引用通常正是正确行为,不能一律视为性能问题。
把整个对象 JSON.stringify 后作为依赖看似比较内容,却引入序列化成本与语义限制,例如函数、循环引用和字段表示规则。深比较同样需要清楚的对象类型与成本边界。更可维护的起点通常是缩小 Effect 的职责,让它只依赖自己实际需要且容易比较的输入。
容易答错的地方
- 为了不重连,删掉对象依赖就行
- Effect 若仍读取这个对象,后续字段变化就可能无法触发必要同步。应该修改代码结构并保持依赖诚实,而不是只删除工具提示的条目。
- 使用 useMemo 后资源一定不会再重建
- 缓存用于减少重复计算,不能承担永久资源身份的正确性保证。Effect 的清理与建立仍应可安全重复,真正资源所有权要由明确的生命周期或稳定持有机制管理。
面试官还会怎么问?
对象 prop 是父组件传来的,该在哪一层修?
先确认子组件需要整个对象还是少数字段。可以让子组件同步直接依赖必要字段,也可以在父层避免无意义的新对象;不要要求所有父组件缓存来掩盖子组件过宽的依赖。
函数体没有读 props,为什么函数依赖还是变?
引用是否相同由函数对象的创建决定,不由函数体是否读取 props 决定。纯工具函数可以在组件外定义,或移到唯一使用它的 Effect 内,减少无关身份变化。
对象内部原地修改,依赖会检测到吗?
如果仍是同一引用,Object.is 不会发现内部字段变化。需要不可变更新或明确的外部订阅机制,不能同时期待引用稳定和任意内部修改都被自动感知。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。