派生状态 $derived
本教程共 50 篇 · 第 7 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:掌握 $derived 和 $derived.by 的用法,理解依赖自动追踪和懒计算机制,搞清楚 $derived 和 $effect 的使用场景区别。学完你能用派生状态简化计算逻辑。
什么是派生状态
有些值不是独立存在的,而是由其他状态计算得来的。比如「两倍计数」依赖于「计数」,「总价」依赖于「单价和数量」。
这种「由其他状态派生出来的值」,就用 $derived 来声明:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
<button onclick={() => count++}>
{count} 的两倍是 {doubled}
</button>
count 变化时,doubled 会自动重新计算。不需要手动同步,也不需要写副作用。
NoteSvelte 组件的代码只在创建时执行一次。如果不用
$derived,doubled的值在count变化后不会更新。$derived让表达式变成「活」的。
$derived 基本语法
$derived 接受一个表达式,返回一个响应式的派生值:
<script>
let firstName = $state('码');
let lastName = $state('上学');
// 派生值:自动追踪依赖
let fullName = $derived(`${firstName}${lastName}`);
let nameLength = $derived(fullName.length);
</script>
<p>姓名:{fullName}({nameLength} 字)</p>
<input bind:value={firstName} placeholder="姓" />
<input bind:value={lastName} placeholder="名" />
几个要点:
$derived里的表达式不应该有副作用(比如count++),Svelte 会阻止在派生表达式中修改状态- 派生值可以依赖其他派生值(如上面的
nameLength依赖fullName) - 和
$state一样,可以在 class 字段中使用$derived
$derived.by:多语句派生
当计算逻辑比较复杂,一个表达式写不下时,用 $derived.by 传入一个函数:
<script>
let numbers = $state([1, 2, 3]);
let total = $derived.by(() => {
let sum = 0;
for (const n of numbers) {
sum += n;
}
return sum;
});
</script>
<p>{numbers.join(' + ')} = {total}</p>
<button onclick={() => numbers.push(numbers.length + 1)}>添加数字</button>
$derived(expression) 本质上等价于 $derived.by(() => expression)。只是短表达式用 $derived 更简洁,复杂逻辑用 $derived.by 更灵活。
依赖自动追踪
$derived 会自动追踪函数体内同步读取的所有响应式值($state、$derived、$props),把它们注册为依赖。当依赖变化时,派生值标记为「脏」,下次读取时重新计算。
<script>
let a = $state(1);
let b = $state(2);
// 依赖 a 和 b
let sum = $derived(a + b);
// 依赖 sum
let message = $derived(`结果是 ${sum}`);
</script>
<p>{message}</p>
<button onclick={() => a++}>a++</button>
<button onclick={() => b++}>b++</button>
Tip依赖追踪是自动的——你在
$derived里读了什么,什么就是依赖。不需要像 React 的useMemo那样手动声明依赖数组。
懒计算(Push-Pull)
Svelte 的响应式采用 push-pull 机制:
- Push(推):状态变化时,立即通知所有依赖它的派生值「你脏了」
- Pull(拉):派生值不会立刻重算,等到被读取时才重新计算
打个比方,就像快递柜——包裹到了先放柜里通知你(push),但你不取就不拆包(pull)。
还有一个优化:如果派生值重算后和之前的值完全相同(引用相等),下游更新会被跳过:
<script>
let count = $state(0);
// large 是布尔值,只有 count 跨过 10 时才会变
let large = $derived(count > 10);
</script>
<!-- count 从 5 变到 6,large 还是 false,按钮文字不会重新渲染 -->
<button onclick={() => count++}>
{large}
</button>
$derived vs $effect
这是初学者最容易混淆的地方。两者的区别可以用一句话概括:
$derived 是用来算值的,$effect 是用来做事的。
| 对比项 | $derived | $effect |
|---|---|---|
| 目的 | 计算并返回值 | 执行副作用 |
| 返回值 | 有(派生值) | 无 |
| 何时运行 | 被读取时(懒计算) | 依赖变化后(立即调度) |
| 能否修改状态 | 不能 | 能(但不推荐) |
| 典型场景 | 计算、过滤、格式化 | DOM 操作、网络请求、日志 |
最常见的错误是用 $effect 来同步状态:
<script>
let count = $state(0);
let doubled = $state(0);
// ❌ 不要这样做!
$effect(() => {
doubled = count * 2;
});
</script>
应该直接用 $derived:
<script>
let count = $state(0);
// ✅ 正确做法
let doubled = $derived(count * 2);
</script>
Note如果你发现自己在
$effect里给某个变量赋值,停下来想想——这个变量是不是可以用$derived代替?大多数情况答案是肯定的。
覆盖派生值
从 Svelte 5.25 起,派生值可以被临时重新赋值(覆盖)。这在做「乐观 UI」时很有用:
<script>
let { post, like } = $props();
// likes 从 post.likes 派生
let likes = $derived(post.likes);
async function onclick() {
// 立即 +1(乐观更新)
likes += 1;
// 通知服务器,成功后 post.likes 会更新,likes 会重新同步
try {
await like();
} catch {
// 失败了,回滚
likes -= 1;
}
}
</script>
<button {onclick}>点赞 {likes}</button>
Tip覆盖只是临时的。当依赖(
post.likes)变化时,派生值会重新计算,覆盖值被丢弃。这正好符合乐观 UI 的需求——先给用户反馈,等服务器结果回来再同步。
解构派生值
$derived 支持解构,解构后的每个变量都是响应式的:
function getCoords() {
return { x: 10, y: 20 };
}
// 三个变量都是响应式的
let { x, y } = $derived(getCoords());
等价于:
let _coords = $derived(getCoords());
let x = $derived(_coords.x);
let y = $derived(_coords.y);
本节回顾
$derived(表达式)创建派生状态,依赖变化时自动重算$derived.by(() => { ... })用于复杂多语句计算- 依赖自动追踪:函数体内同步读取的响应式值都是依赖
- Push-Pull 机制:变化时标记脏(push),读取时才重算(pull)
$derived算值,$effect做事,不要用$effect同步状态- 派生值可被临时覆盖(Svelte 5.25+),适合乐观 UI 场景
- 解构
$derived返回的对象,每个变量仍是响应式的