useEffect 与 watch
React 使用 useEffect 同步外部系统;Vue 使用 watch、watchEffect 或生命周期 API 处理对应场景。
核心结论
Effect 不是“状态变化后的通用代码区”,而是把组件状态与网络连接、浏览器 API、订阅等外部系统同步。能够在 render 或 computed 中得到的值,不应再用 Effect 保存一份。
连接与清理
组件关系与数据流
StateComponentEffectConnection
差异焦点useEffect 属于提交后的同步阶段,依赖必须手动列出。
实线表示依赖、数据或命令传递,虚线表示事件、回调或清理路径。
flowchart TB
accTitle: React useEffect 提交后同步机制
accDescr: roomId 更新先让 ReactDemo 重新渲染并提交 DOM,提交完成后 React 比较依赖数组,清理旧 Effect 再连接新房间。
room(["roomId state"]) --> demo["ReactDemo render"]
demo --> effect{{"commit 后比较 [roomId]"}}
effect -.->|"先执行 cleanup"| old[("旧 Connection")]
effect -->|"再 connect"| current[("新 Connection")]
current --> status(["status + events"])
class room,status state
class demo component
class effect process
class old,current resource
classDef state fill:#fffdf8,stroke:#149eca,color:#25221d,stroke-width:1.2px
classDef component fill:#e4f5fa,stroke:#149eca,color:#25221d,stroke-width:2px,font-weight:600
classDef process fill:#f4fafb,stroke:#149eca,color:#25221d,stroke-width:1.6px
classDef resource fill:#eee9df,stroke:#817a70,color:#25221d,stroke-width:1.4pxstate 更新先完成 render/commit,再按依赖数组执行 cleanup 和新 Effect。
组件关系与数据流
StateComponentEffectConnection
差异焦点watch 直接监听 ref,依赖由响应式 source 表达。
实线表示依赖、数据或命令传递,虚线表示事件、回调或清理路径。
flowchart TB
accTitle: Vue watch 响应式监听机制
accDescr: watch 直接订阅 roomId ref,值变化时先执行 onCleanup,再运行回调创建新连接;无需等待整个组件重新执行。
room(["roomId ref"]) --> effect{{"watch 订阅响应式 source"}}
effect -.->|"onCleanup"| old[("旧 Connection")]
effect -->|"回调 connect"| current[("新 Connection")]
current --> status(["status + events refs"])
status --> demo["VueDemo template"]
class room,status state
class demo component
class effect process
class old,current resource
classDef state fill:#fffdf8,stroke:#2f9d6b,color:#25221d,stroke-width:1.2px
classDef component fill:#e4f4eb,stroke:#2f9d6b,color:#25221d,stroke-width:2px,font-weight:600
classDef process fill:#f2f8f4,stroke:#2f9d6b,color:#25221d,stroke-width:1.6px
classDef resource fill:#eee9df,stroke:#817a70,color:#25221d,stroke-width:1.4pxwatch 订阅明确的响应式 source,变化时执行 cleanup 与回调。
完整 Demo 源码
以下代码直接读取在线 Demo 使用的源文件,没有省略或改写。
React 源码
import { useEffect, useRef, useState } from "react";type RoomId = "general" | "react" | "vue";type ConnectionEvent = { id: number; message: string;};const rooms: { id: RoomId; label: string }[] = [ { id: "general", label: "综合讨论" }, { id: "react", label: "React 交流" }, { id: "vue", label: "Vue 交流" },];function createConnection(roomId: RoomId, notify: (message: string) => void) { const roomName = rooms.find((room) => room.id === roomId)?.label ?? roomId; let timer: number | undefined; return { connect() { notify(`正在连接:${roomName}`); timer = window.setTimeout(() => notify(`已连接:${roomName}`), 400); }, disconnect() { window.clearTimeout(timer); notify(`已断开:${roomName}`); }, };}export function ReactDemo() { const [roomId, setRoomId] = useState<RoomId>("general"); const [status, setStatus] = useState("等待连接"); const [events, setEvents] = useState<ConnectionEvent[]>([]); const nextEventId = useRef(1); useEffect(() => { const connection = createConnection(roomId, (message) => { const event = { id: nextEventId.current++, message }; setStatus(message); setEvents((current) => [event, ...current].slice(0, 5)); }); connection.connect(); return connection.disconnect; }, [roomId]); return ( <div className="demo-stack"> <label className="demo-field"> 当前房间 <select className="demo-select" value={roomId} onChange={(event) => setRoomId(event.target.value as RoomId)} > {rooms.map((room) => ( <option key={room.id} value={room.id}> {room.label} </option> ))} </select> </label> <div className="demo-card"> <span className="demo-tag">连接状态</span> <h2>{status}</h2> <p>切换房间会先清理旧连接,再建立新连接。</p> </div> <p className="demo-muted">连接日志(最近 5 条)</p> <ul className="demo-list" aria-live="polite"> {events.map((event) => ( <li key={event.id}>{event.message}</li> ))} </ul> </div> );}Vue 3 源码
<script setup lang="ts">import { ref, watch } from "vue";type RoomId = "general" | "react" | "vue";type ConnectionEvent = { id: number; message: string;};const rooms: { id: RoomId; label: string }[] = [ { id: "general", label: "综合讨论" }, { id: "react", label: "React 交流" }, { id: "vue", label: "Vue 交流" },];function createConnection(roomId: RoomId, notify: (message: string) => void) { const roomName = rooms.find((room) => room.id === roomId)?.label ?? roomId; let timer: number | undefined; return { connect() { notify(`正在连接:${roomName}`); timer = window.setTimeout(() => notify(`已连接:${roomName}`), 400); }, disconnect() { window.clearTimeout(timer); notify(`已断开:${roomName}`); }, };}const roomId = ref<RoomId>("general");const status = ref("等待连接");const events = ref<ConnectionEvent[]>([]);let nextEventId = 1;watch( roomId, (currentRoomId, _, onCleanup) => { const connection = createConnection(currentRoomId, (message) => { status.value = message; events.value = [ { id: nextEventId++, message }, ...events.value, ].slice(0, 5); }); connection.connect(); onCleanup(connection.disconnect); }, { immediate: true },);</script><template> <div class="demo-stack"> <label class="demo-field"> 当前房间 <select v-model="roomId" class="demo-select"> <option v-for="room in rooms" :key="room.id" :value="room.id"> {{ room.label }} </option> </select> </label> <div class="demo-card"> <span class="demo-tag">连接状态</span> <h2>{{ status }}</h2> <p>切换房间会先清理旧连接,再建立新连接。</p> </div> <p class="demo-muted">连接日志(最近 5 条)</p> <ul class="demo-list" aria-live="polite"> <li v-for="event in events" :key="event.id"> {{ event.message }} </li> </ul> </div></template>在线观察
切换聊天室,观察旧连接总是在新连接建立前清理。这个示例依赖浏览器运行环境,因此使用纯客户端加载。
React
Vue 3
API 对照
| React | Vue | 适用场景 |
|---|---|---|
useEffect(fn, [source]) | watch(source, fn) | 明确监听某个状态 |
useEffect(fn, []) | onMounted / onUnmounted | 组件生命周期 |
| 手动列出多个依赖 | watchEffect 自动追踪读取 | 同步依赖较多的副作用 |
return cleanup | onCleanup | 下次执行或卸载前清理 |
React Strict Mode
开发模式可能额外执行一次“连接 → 清理 → 再连接”,用于检查清理逻辑是否完整。正确的 Effect 应能安全地重复建立和销毁。
服务端数据请求通常交给TanStack Query;纯派生计算应回到派生状态。