首页 / Svelte 5 入门教程 / 副作用 $effect(上)

Svelte 5 入门教程

副作用 $effect(上)

本教程共 50 篇 · 第 8 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

SvelteSvelte 5Runes$effect副作用

本节目标:掌握 $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 的函数会自动追踪读取的响应式值(colorsize),当它们变化时重新执行。

Note

$effect 只在浏览器中运行,不会在服务端渲染时执行。所以里面可以安全地访问 windowdocument 等浏览器 API。

基本语法

<script>
	let count = $state(0);

	$effect(() => {
		// 函数体内同步读取的响应式值都是依赖
		console.log(`count 变成了 ${count}`);
	});
</script>

$effect 在组件挂载到 DOM 后首次执行,之后每当依赖变化时重新执行。

清理函数(Cleanup)

$effect 可以返回一个函数作为清理逻辑。这个清理函数在两种时机执行:

  1. effect 重新运行之前
  2. 组件销毁时

经典的定时器例子:

<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>

执行流程是这样的:

  1. 组件挂载 → 创建定时器(1000ms)
  2. 点击「放慢」→ milliseconds 变成 2000 → 清理旧定时器 → 创建新定时器(2000ms)
  3. 组件销毁 → 清理当前定时器
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();
	}
});
  • conditiontrue 时:conditioncolor 都是依赖
  • conditionfalse 时:只有 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 的执行时机:

  1. 首次执行:组件挂载到 DOM 之后
  2. 重运行:依赖变化后的微任务中,DOM 更新之后

多次状态变化会被批量处理。也就是说,同时修改 colorsize 不会触发两次 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 只运行一次