跳转到内容

useEffect 与 watch

切换聊天室时,需要先断开旧连接再建立新连接。这个 Demo 对比两边如何执行副作用、声明依赖并注册清理函数。

React

useEffect 根据依赖重新连接,并在返回函数中清理旧连接。

在线预览

组件关系与数据流

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:#ffffff,stroke:#149eca,color:#172033,stroke-width:1.2px
  classDef component fill:#e5f6fb,stroke:#149eca,color:#172033,stroke-width:2px,font-weight:600
  classDef process fill:#f4fbfd,stroke:#149eca,color:#172033,stroke-width:1.6px
  classDef resource fill:#f3f5f8,stroke:#778195,color:#172033,stroke-width:1.4px

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

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

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

实际运行源码

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

watch 显式监听房间,并通过 onCleanup 清理上一次连接。

在线预览

组件关系与数据流

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:#ffffff,stroke:#42b883,color:#172033,stroke-width:1.2px
  classDef component fill:#e7f8f0,stroke:#42b883,color:#172033,stroke-width:2px,font-weight:600
  classDef process fill:#f2fbf7,stroke:#42b883,color:#172033,stroke-width:1.6px
  classDef resource fill:#f3f5f8,stroke:#778195,color:#172033,stroke-width:1.4px

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

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

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

实际运行源码

<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 适用场景
useEffect(fn, [roomId]) watch(roomId, fn, { immediate: true }) 明确监听某个状态
useEffect(fn, []) onMounted + onUnmounted 只关心挂载和卸载
useEffect 读取多个依赖 watchEffect Vue 自动追踪回调中读取的响应式状态
return cleanup onCleanup 下次执行或组件卸载前清理

React 把副作用统一为“渲染后与外部系统同步”,依赖需要显式列出;Vue 则按意图拆分为 watchwatchEffect 和生命周期 API。