副作用 $effect(下)
本教程共 50 篇 · 第 9 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
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 | 执行时机 | 自动清理 | 常用程度 |
|---|---|---|---|
$effect | DOM 更新后 | 是(跟随组件) | 常用 |
$effect.pre | DOM 更新前 | 是(跟随组件) | 偶尔用 |
$effect.tracking() | 立即返回 | — | 很少用 |
$effect.root | 立即执行内部 effect | 否(手动清理) | 很少用 |
本节回顾
$effect.pre在 DOM 更新前执行,适合读取旧 DOM 状态的场景$effect.tracking()检测当前是否在追踪上下文中,主要用于库开发$effect.root创建不绑定组件的 effect 作用域,需手动清理- 不要用
$effect同步状态——用$derived代替 - 不要用
$effect处理双向联动——用$derived+ 函数绑定 - 不要用
$effect响应用户事件——用事件处理函数 $effect是逃生舱,适合 DOM 操作、第三方库集成、日志统计