React × Vue

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.4px

state 更新先完成 render/commit,再按依赖数组执行 cleanup 和新 Effect。

React 流程图
React 组件关系与数据流

滚轮缩放 · 按住拖动 · 双击重置 · Esc 关闭

组件关系与数据流

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.4px

watch 订阅明确的响应式 source,变化时执行 cleanup 与回调。

Vue 3 流程图
Vue 3 组件关系与数据流

滚轮缩放 · 按住拖动 · 双击重置 · Esc 关闭

完整 Demo 源码

以下代码直接读取在线 Demo 使用的源文件,没有省略或改写。

React 源码

ReactDemo.tsx
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 源码

VueDemo.vue
<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 对照

ReactVue适用场景
useEffect(fn, [source])watch(source, fn)明确监听某个状态
useEffect(fn, [])onMounted / onUnmounted组件生命周期
手动列出多个依赖watchEffect 自动追踪读取同步依赖较多的副作用
return cleanuponCleanup下次执行或卸载前清理

React Strict Mode

开发模式可能额外执行一次“连接 → 清理 → 再连接”,用于检查清理逻辑是否完整。正确的 Effect 应能安全地重复建立和销毁。

服务端数据请求通常交给TanStack Query;纯派生计算应回到派生状态。