首页 / Svelte 5 入门教程 / 派生状态 $derived

Svelte 5 入门教程

派生状态 $derived

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

SvelteSvelte 5Runes$derived响应式

7. 派生状态 $derived

本节目标:掌握 $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 会自动重新计算。不需要手动同步,也不需要写副作用。

Note

Svelte 组件的代码只在创建时执行一次。如果不用 $deriveddoubled 的值在 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 返回的对象,每个变量仍是响应式的