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

Svelte 5 入门教程

副作用 $effect(下)

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

SvelteSvelte 5Runes$effect进阶

9. 副作用 $effect(下)

本节目标:掌握 $effect.pre、$effect.tracking、$effect.root 三个高级变体,理解何时不该用 $effect 以及替代方案。学完你能精准控制副作用的执行时机和作用域。

$effect.pre:DOM 更新前执行

普通 $effect 在 DOM 更新之后运行。但有时候你需要在 DOM 更新之前做点什么,比如自动滚动到底部。

$effect.pre 就是干这个的:

<script>
	import { tick } from 'svelte';

	let div;
	let messages = $state([
		'你好',
		'今天天气不错',
		'一起学 Svelte 吧'
	]);

	function addMessage() {
		messages.push(`消息 ${messages.length + 1}`);
	}

	$effect.pre(() => {
		if (!div) return; // 还没挂载,跳过

		// 引用 messages.length 让这个 effect 依赖它
		messages.length;

		// 如果用户在底部附近,自动滚动到底
		if (div.offsetHeight + div.scrollTop > div.scrollHeight - 20) {
			tick().then(() => {
				div.scrollTo(0, div.scrollHeight);
			});
		}
	});
</script>

<div bind:this={div} style="height: 100px; overflow-y: auto; border: 1px solid #ccc;">
	{#each messages as message}
		<p>{message}</p>
	{/each}
</div>
<button onclick={addMessage}>添加消息</button>

$effect.pre$effect 的唯一区别就是执行时机。其他行为(依赖追踪、清理函数、批量处理)完全一样。

Note

为什么要分 DOM 前/后?因为有些操作需要在 DOM 更新前读取旧状态(如旧的滚动位置),有些需要在更新后操作新 DOM(如获取新元素)。大多数场景用 $effect 就够了,$effect.pre 是少见的特殊需求。

$effect.tracking:检测追踪状态

$effect.tracking() 返回一个布尔值,告诉你当前代码是否运行在追踪上下文中:

<script>
	console.log('组件初始化时:', $effect.tracking()); // false

	$effect(() => {
		console.log('effect 内部:', $effect.tracking()); // true
	});
</script>

<!-- 模板中也是追踪上下文 -->
<p>模板中: {$effect.tracking()}</p> <!-- true -->

这个 API 主要用于库开发者。比如你想创建一个「只在被追踪时才订阅」的工具函数:

import { createSubscriber } from 'svelte/reactivity';

// createSubscriber 内部使用 $effect.tracking 来决定是否订阅
const prefersDarkMode = createSubscriber((set) => {
	// 只有当有人在 effect 或模板中读取这个值时,才订阅 media query
	const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

	const update = () => set(mediaQuery.matches);
	update();
	mediaQuery.addEventListener('change', update);

	return () => mediaQuery.removeEventListener('change', update);
});
Tip

日常开发中你几乎不会直接用 $effect.tracking()。了解它的存在就好,遇到需要「按需订阅」的场景再回来查。

$effect.root:脱离组件的作用域

$effect 默认和组件生命周期绑定——组件销毁时,所有 effect 自动清理。但有时候你想创建独立于组件的 effect,手动控制清理。

$effect.root 创建一个不自动清理的作用域:

const destroy = $effect.root(() => {
	$effect(() => {
		console.log('这个 effect 不绑定任何组件');
		// 只有手动调用 destroy() 才会清理
	});

	return () => {
		console.log('清理所有内部 effect');
	};
});

// 需要清理时手动调用
destroy();

使用场景:

  • .svelte.js 模块文件中创建 effect(没有组件上下文)
  • 创建需要手动管理生命周期的响应式工具
Note

$effect.root 是高级 API,日常开发很少用。它主要用于库和工具函数的实现,普通业务组件不需要。

何时不该用 $effect

$effect 应该被视为逃生舱,而不是常规工具。下面几种场景就不该用 $effect

场景一:同步状态

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

	// ❌ 不应该用 effect 来同步状态
	$effect(() => {
		doubled = count * 2;
	});
</script>

$derived 代替:

<script>
	let count = $state(0);
	let doubled = $derived(count * 2); // ✅
</script>

场景二:双向联动

两个值互相依赖时,用 effect 会导致循环问题:

<script>
	const total = 100;
	let spent = $state(0);
	let left = $state(total);

	// ❌ 两个 effect 互相触发
	$effect(() => { left = total - spent; });
	$effect(() => { spent = total - left; });
</script>

$derived 和函数绑定代替:

<script>
	const total = 100;
	let spent = $state(0);
	let left = $derived(total - spent); // ✅ 派生值

	function updateLeft(newLeft) {
		spent = total - newLeft; // ✅ 函数绑定
	}
</script>

<label>
	<input type="range" bind:value={spent} max={total} />
	{spent}/{total} 已花费
</label>

<label>
	<input type="range" bind:value={() => left, updateLeft} max={total} />
	{left}/{total} 剩余
</label>

场景三:响应用户事件

有些逻辑只在用户操作时才需要执行,不需要响应式追踪:

<script>
	let name = $state('');

	// ❌ 不需要 effect
	$effect(() => {
		if (name) {
			saveToServer(name);
		}
	});
</script>

直接在事件处理中调用:

<script>
	let name = $state('');

	async function handleSubmit() {
		// ✅ 直接在事件处理中调用
		await saveToServer(name);
	}
</script>

$effect 适合做什么

适合用 $effect不适合用 $effect
操作 DOM(canvas 绘制、焦点管理)同步状态(用 $derived)
集成第三方库(初始化、销毁)响应事件(用事件处理函数)
日志、分析统计双向联动(用函数绑定)
定时器、事件监听器数据获取(用 SvelteKit 的 load)
Tip

记住一个原则:能用 $derived 解决的,就别用 $effect$effect 是当你确实需要「对外部世界产生影响」时才用的。

$effect 全家福

Rune执行时机自动清理常用程度
$effectDOM 更新后是(跟随组件)常用
$effect.preDOM 更新前是(跟随组件)偶尔用
$effect.tracking()立即返回很少用
$effect.root立即执行内部 effect否(手动清理)很少用

本节回顾

  • $effect.pre 在 DOM 更新前执行,适合读取旧 DOM 状态的场景
  • $effect.tracking() 检测当前是否在追踪上下文中,主要用于库开发
  • $effect.root 创建不绑定组件的 effect 作用域,需手动清理
  • 不要用 $effect 同步状态——用 $derived 代替
  • 不要用 $effect 处理双向联动——用 $derived + 函数绑定
  • 不要用 $effect 响应用户事件——用事件处理函数
  • $effect 是逃生舱,适合 DOM 操作、第三方库集成、日志统计