先记住这个答案
组件首次提交后,Effect 建立同步。如果后续提交中依赖发生变化,React 先运行上一轮返回的 cleanup,它捕获的是上一轮的输入与资源,然后运行新一轮 setup。卸载时也需要清理仍然存在的同步。普通依赖未变的更新不会仅因组件重新渲染就重建该 Effect。理解时最好把它看成多个独立的同步周期,而不是把清理简单等同于组件卸载。
- cleanup 不只在卸载时运行,也在重同步前运行
- 清理应释放本轮建立的资源,而不是猜测当前资源
- 开发 Strict Mode 会检查建立与清理是否能正确配对
切换文档时,清理函数为什么仍然知道旧 ID
Effect 内创建的 cleanup 是那一轮执行时形成的闭包,它可以直接引用同轮的连接或取消函数。文档 ID 从 A 变成 B 后,清理 A 的逻辑无需从最新 props 再判断要关闭哪个连接;它已经持有本轮创建的资源。随后新的 setup 才根据 B 建立下一轮。
如果把连接放进共享可变变量,又在清理时通过这个变量读取,后续代码可能已经把它改成另一个资源,导致关闭错误连接。通常更清楚的写法是在 Effect 内创建局部句柄,并让返回的函数关闭同一个句柄。确需跨流程共享时,也必须说明所有权与替换规则。
依赖未变与重新挂载是两种不同情况
组件因无关状态更新而重新渲染,如果这一 Effect 的依赖仍相同,就无需因为这次普通更新重新建立同步。可是一旦组件被移除再重新挂载,新的实例就需要新的同步周期,即使业务参数与之前相同。key 改变造成的身份变化也可能引起这种重建。
开发 Strict Mode 的额外建立、清理检查用于暴露不对称资源管理。例如建立了两个监听,却只移除一个,重复挂载后便可能出现同一事件处理两次。用 ref 标记跳过第二次建立只是在遮住检查信号,并没有证明真实路由切换时资源会正确释放。
异步工作还需要处理清理后的迟到结果
cleanup 可以取消订阅、清理定时器,或向可取消请求发送中止信号,但不代表所有外部工作都已停止。服务端可能已经完成操作,某些 API 也不支持取消。因此接收异步结果时还需要判断它是否属于当前有效的一轮,避免旧文档结果写入新文档界面。
也不要把 Effect 函数直接声明成 async 后返回 Promise,把它误当作清理函数。可以在 Effect 内启动异步函数,同时同步返回清理逻辑;对取消、错误与当前轮次分别处理。验证时应故意让旧请求比新请求晚返回,检查页面是否仍显示当前对象的数据。
容易答错的地方
- cleanup 执行说明组件一定卸载了
- 依赖变化引起重同步前也会清理旧一轮,开发检查也可能触发清理。日志应带上具体资源和参数,才能区分是在切换同步对象还是结束整个组件生命周期。
- 清理时把所有共享资源都关闭最保险
- 共享资源可能仍被其它组件使用。清理应释放本轮拥有的句柄或订阅份额,连接池与全局服务则按其协议管理,不能由一个消费者卸载就无条件销毁所有者的资源。
面试官还会怎么问?
cleanup 可以读取最新 state 吗?
它直接闭包捕获的是对应渲染与建立周期的值。确实需要读取额外最新信息时,要明确使用何种机制以及为什么需要,但释放本轮资源通常应依据本轮句柄。
空依赖的 Effect 还需要 cleanup 吗?
需要与否由它是否建立了应释放的资源决定。空数组不会让事件监听或定时器自动消失,组件卸载时仍需正确结束这些外部关系。
多个 Effect 能靠执行顺序传递中间结果吗?
不宜把隐式顺序当成业务协议。存在同一资源生命周期依赖的工作可放在清楚的同一过程里;独立过程则各自声明输入,避免通过可变共享变量耦合。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。