先记住这个答案
省略第二个参数时,Effect 会在组件每次提交后重新运行;传空数组时,不会因为普通响应式依赖变化而重新同步,但仍有首次建立、卸载清理及开发检查;列出依赖时,首次建立之后,只要某项与上次通过 Object.is 比较不同,就需要先清理旧同步再建立新同步。依赖应由代码实际读取的值决定,不能只按希望执行几次来随意选择。
- 比较发生在依赖项之间,不是比较依赖数组对象身份
- 空数组不等于应用整个生命周期只执行一次
- 依赖变化先清理旧一轮,再建立新一轮
省略参数为什么容易产生反馈循环
例如 Effect 每次提交后都调用 setter 保存一个新对象,更新又产生新提交,随后 Effect 再更新,就形成反馈循环。问题不是省略依赖在语法上非法,而是同步逻辑不断制造新的输入变化。先判断那个 state 是否必要,再决定依赖与更新方式,通常比直接补一个空数组更有效。
同时要准确使用“提交”一词:组件函数可能在开发检查或可中断渲染中执行,但未必都形成独立的已提交更新。观察日志时应把渲染计算和 Effect 执行分开,避免根据函数调用次数写出与实际生命周期不一致的判断。
空数组的约定不包含永久唯一执行
一个只使用固定浏览器事件名和本轮内部变量的订阅,可以没有动态依赖;它在挂载后建立,在卸载时清理。但切换路由后重新挂载会建立新的订阅,开发环境的 Strict Mode 检查也可能带来额外建立与清理周期。空数组不能充当全局去重或支付防重机制。
如果 Effect 内部读取 roomId,却传空数组,组件后续切到新房间时仍可能保持旧连接。此时不是 React 忽略了变化,而是依赖声明没有描述实际输入。不要把首次值恰好正确当作整个生命周期都正确,应通过修改输入与重新挂载两种操作分别验证。
列出依赖时关注每项值如何产生
依赖列表可以每次写成数组字面量,React 比较的是对应位置的每一项,而不是数组容器的引用。数字和字符串按 Object.is 判断值,对象与函数通常需要关注引用是否变化。如果配置对象每次重新创建,即便字段一样,也可能使同步不断重建。
依赖的数量与顺序应该固定并直接写出,便于工具分析。确实不想因某项输入重连时,应重新检查代码结构,例如把只需要在同步内部创建的对象移进去,或分开两个独立同步过程;不能用条件拼接依赖数组让 React 猜测当前是哪种同步约定。
容易答错的地方
- 空数组会在服务端和客户端各执行一次
- Effect 不在服务端渲染期间执行。客户端的挂载与开发检查需要单独理解,不能把服务器输出 HTML 也算成一次 Effect 建立。
- 依赖数组每次新建,所以 Effect 总会重跑
- React 比较各个依赖项,不是这个数组对象本身。真正值得检查的是数组中的对象、函数或其它值是否变化,以及是否存在省略依赖参数的情况。
面试官还会怎么问?
依赖没变化时会先运行 cleanup 吗?
普通的同实例更新中,如果这一轮不需要重新同步,就不会仅因这次更新而执行该 Effect 的清理。卸载、开发检查等生命周期情形需要另外考虑。
依赖从 0 变成 -0 会被认为变化吗?
Object.is 能区分正零与负零,因此这两个依赖值不同;它也把 NaN 与 NaN 视为相同。多数业务不需要利用这种边界,但可以用它说明这里不是简单的等号比较。
想只记录首次打开页面,可以用空数组吗?
可以用挂载相关逻辑表达某个视图的展示,但仍要定义重新进入、开发检查和统计去重规则。可靠的事件口径不能只靠 Effect 看似运行一次来保证。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。