副作用 $effect(上)
本教程共 50 篇 · 第 8 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:掌握 $effect 的基础语法、清理函数、依赖追踪机制和无限循环问题。学完你能安全地使用 $effect 处理 DOM 操作和第三方库集成。
$effect 是什么
$effect 用来执行副作用——那些「不只是计算值,还要对外部世界产生影响」的操作。比如操作 DOM、调用第三方库、发起网络请求等。
先看一个画 canvas 的例子:
<script>
let size = $state(50);
let color = $state('#ff3e00');
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// color 或 size 变化时,重新绘制
context.fillStyle = color;
context.fillRect(0, 0, size, size);
});
</script>
<canvas bind:this={canvas} width="100" height="100"></canvas>
<input type="color" bind:value={color} />
<input type="range" bind:value={size} min="10" max="100" />
$effect 的函数会自动追踪读取的响应式值(color、size),当它们变化时重新执行。
Note
$effect只在浏览器中运行,不会在服务端渲染时执行。所以里面可以安全地访问window、document等浏览器 API。
基本语法
<script>
let count = $state(0);
$effect(() => {
// 函数体内同步读取的响应式值都是依赖
console.log(`count 变成了 ${count}`);
});
</script>
$effect 在组件挂载到 DOM 后首次执行,之后每当依赖变化时重新执行。
清理函数(Cleanup)
$effect 可以返回一个函数作为清理逻辑。这个清理函数在两种时机执行:
- effect 重新运行之前
- 组件销毁时
经典的定时器例子:
<script>
let count = $state(0);
let milliseconds = $state(1000);
$effect(() => {
// 每当 milliseconds 变化时,创建新的定时器
const interval = setInterval(() => {
count += 1;
}, milliseconds);
// 清理函数:重新运行前或组件销毁时执行
return () => {
clearInterval(interval);
};
});
</script>
<h1>{count}</h1>
<button onclick={() => milliseconds *= 2}>放慢</button>
<button onclick={() => milliseconds /= 2}>加快</button>
执行流程是这样的:
- 组件挂载 → 创建定时器(1000ms)
- 点击「放慢」→ milliseconds 变成 2000 → 清理旧定时器 → 创建新定时器(2000ms)
- 组件销毁 → 清理当前定时器
Tip打个比方,清理函数就像退房时交还钥匙。每次换房(effect 重新运行)前先交还旧钥匙,离开酒店(组件销毁)时也要交还。
依赖追踪机制
$effect 自动追踪函数体内同步读取的响应式值。关键词是「同步」——异步代码(await 之后、setTimeout 回调里)读取的值不会被追踪。
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// color 是同步读取的 → 会被追踪
context.fillStyle = color;
setTimeout(() => {
// size 是异步读取的 → 不会被追踪!
context.fillRect(0, 0, size, size);
}, 0);
});
上面这个 effect,color 变化会触发重绘,但 size 变化不会。
条件依赖
effect 只追踪上次运行时实际读取的值。如果某次运行因为条件分支没有读取某个值,那这个值就不再是依赖:
let condition = $state(true);
let color = $state('#ff3e00');
$effect(() => {
if (condition) {
// condition 为 true 时,color 被读取 → color 是依赖
confetti({ colors: [color] });
} else {
// condition 为 false 时,color 没被读取 → color 不是依赖
confetti();
}
});
condition为true时:condition和color都是依赖condition为false时:只有condition是依赖,color变化不触发重运行
对象级别的追踪
effect 追踪的是读取了哪个值,而不是值内部的属性变化:
let state = $state({ value: 0 });
// 只读取了 state 本身(没变过引用),只运行一次
$effect(() => {
state;
});
// 读取了 state.value,每次 value 变化都重新运行
$effect(() => {
state.value;
});
避免无限循环
$effect 最常见的坑是无限循环:在 effect 里修改了自己依赖的状态。
<script>
let count = $state(0);
// ❌ 无限循环!
$effect(() => {
count++; // 修改了 count,触发 effect 重运行
// effect 重运行又修改 count... 死循环
});
</script>
规则:不要在 $effect 中修改自己依赖的状态。
如果确实需要在 effect 里修改状态,而且这个状态恰好是依赖,用 untrack 避免追踪:
import { untrack } from 'svelte';
$effect(() => {
const value = someState; // 追踪 someState
untrack(() => {
// otherState 的修改不会触发这个 effect 重运行
otherState = value * 2;
});
});
Note大多数情况下,如果你发现自己在 effect 里修改状态,说明你可能应该用
$derived代替。回头看第 7 章的「$derived vs $effect」对比。
$effect 只在浏览器运行
$effect 在服务端渲染时不会执行。这是有意设计的——服务端没有 DOM、没有 window、没有定时器。
<script>
let data = $state(null);
$effect(() => {
// 这段代码只在浏览器中运行
// 服务端渲染时不会执行
fetch('/api/data')
.then(res => res.json())
.then(json => data = json);
});
</script>
Tip如果你需要在组件挂载后做一些初始化操作(如读取 localStorage、设置事件监听器),
$effect是正确的选择。
执行时机
$effect 的执行时机:
- 首次执行:组件挂载到 DOM 之后
- 重运行:依赖变化后的微任务中,DOM 更新之后
多次状态变化会被批量处理。也就是说,同时修改 color 和 size 不会触发两次 effect 运行,只会在一次微任务中执行一次。
<script>
let a = $state(1);
let b = $state(2);
$effect(() => {
console.log(`a=${a}, b=${b}`);
});
function updateBoth() {
a = 10; // 不会立即触发 effect
b = 20; // 批量处理,只触发一次
// 输出:a=10, b=20(只打印一次)
}
</script>
<button onclick={updateBoth}>同时更新</button>
本节回顾
$effect(() => { ... })创建副作用,自动追踪同步读取的响应式值- 返回的清理函数在 effect 重运行前和组件销毁时执行
- 异步代码(await 后、setTimeout 回调)中读取的值不被追踪
- 条件分支会影响依赖——没读到的值不是依赖
- 不要在 effect 中修改自己依赖的状态,会导致无限循环
$effect只在浏览器运行,服务端不执行- 多次状态变化会被批量处理,effect 只运行一次