先记住这个答案
React 可以根据 props 和 state 计算 JSX,却不会自动控制所有命令式系统,例如浏览器事件订阅、第三方地图实例或实时连接。Effect 用来建立这些外部关系,并在相关输入变化后更新或重新建立,必要时清理旧资源。它不是“渲染后任意执行代码”的默认位置:能直接计算的展示值留在渲染中,由一次点击引起的命令通常留在事件处理器中。
- 外部系统不只指远程服务,也包括浏览器 API 和第三方实例
- 同步关系应说明输入、建立方式与结束方式
- 纯计算、用户命令和持续同步分别表达
为什么 JSX 不能自动控制第三方地图
假设页面 state 保存 zoom,而第三方地图只提供 setZoom 这样的命令式接口。渲染一个显示 zoom 的文本,只会改变 React 管理的界面,不会自动通知地图实例。同步逻辑需要把当前 zoom 传给地图 API,使地图视角与页面输入对应起来。
此处“外部”指不由 React 的声明式渲染直接管理,并不要求跨网络。浏览器事件源、媒体播放对象、定时器和嵌入式编辑器都可能属于这一类。反过来,筛选数组虽然是额外计算,却不因为写在 JSX 外面就成为外部系统,通常可以直接根据输入计算。
把资源建立与参数同步分清楚
一个地图实例可能在组件挂载期间保留,而缩放级别会多次变化。应根据地图 API 的生命周期区分创建实例、更新参数和释放实例,避免每次 zoom 改变都销毁重建整个地图。若资源只在一个同步周期内有效,就让该轮清理持有它自己的句柄。
订阅当前文档则可能在 documentId 变化时需要结束旧连接并建立新连接。两种例子都属于同步,但资源是否可复用、哪些输入要求重建并不相同。不能机械复制一个空依赖初始化加一个万能更新 Effect,而应先看外部 API 对实例和参数的实际约束。
用输入变化与退出场景检验同步设计
验收时除了首次显示,还应切换外部对象、修改配置、离开页面再返回,确认资源始终对应当前输入,并且旧一轮不再影响界面。异步结果可能迟到,应检查取消与轮次判断;共享资源则应区分所有者销毁与消费者取消订阅。
Effect 只在客户端执行,因此服务端 HTML 中需要的正文和初始数据不能等待它补齐。客户端资源可以在挂载后连接,但初始界面应有清楚的可用状态。若任务只是点击按钮发送一条消息,也不必先把消息命令转换成 state 再由 Effect 猜测是否该发送。
容易答错的地方
- 凡是 render 里不方便写的代码都放 Effect
- 位置不方便不是同步需求的证据。先判断代码是纯计算、用户动作还是与外部资源维持关系,再选择适合的组织方式,避免 Effect 之间不断设置状态形成链条。
- 没有网络请求就没有外部系统
- 外部系统也可能就在当前浏览器中。全局事件、第三方控件和定时器各有自己的生命周期,React 不会因为组件返回了一段 JSX 就自动建立或解除这些关系。
面试官还会怎么问?
同步外部控件都需要返回 cleanup 吗?
取决于建立了什么资源。监听、连接和需要显式销毁的实例应配对释放;只调用某个幂等更新方法且没有额外持有资源的同步,不一定需要虚构一个清理操作。
为什么不在组件函数里直接连接?
渲染可能被重复执行或放弃,函数体应保持纯粹。提前建立连接会让外部资源与未提交的渲染纠缠,也缺少清楚的清理配对,因此应使用合适的生命周期管理。
两个同步过程可以写在同一个 Effect 吗?
可以,但应确认它们确实共享同一重同步条件和资源生命周期。无关关系混在一起会让一个输入变化重复执行另一件事,分开往往更容易声明正确依赖与清理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。