找回密码
 立即注册
首页 业界区 业界 Svelte 最新中文文档翻译(3)—— 符文(Runes)上 ...

Svelte 最新中文文档翻译(3)—— 符文(Runes)上

桂册 2025-6-6 10:06:52
前言

Svelte,一个非常“有趣”、用起来“很爽”的前端框架。从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1
1.png

Svelte 以其独特的编译时优化机制著称,具有轻量级高性能易上手等特性,非常适合构建轻量级 Web 项目,也是我做个人项目的首选技术栈。
目前 Svelte 基于 Svelte 5 发布了最新的官方文档,但却缺少对应的中文文档。为了帮助大家学习 Svelte,为爱发电翻译了官方文档。
我同时搭建了 Svelte 最新的中文文档站点:https://svelte.yayujs.com ,如果需要辅助学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!
虽说是翻译,但个人并不喜欢严格遵守原文,为了保证中文阅读流畅,会删减部分语句,对难懂的部分也会另做补充解释,希望能给大家带来一个好的中文学习体验。
欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。
什么是符文?

[!NOTE] 符文 /ro͞on/ 名词
一个用作神秘或魔法符号的字母或标记。
符文是你在 .svelte 和 .svelte.js/.svelte.ts 文件中用来控制 Svelte 编译器的符号。如果你把 Svelte 看作一门语言,符文就是语法的一部分 — 它们是 关键字
符文有一个 $ 前缀,看起来像函数:
  1. let message = $state('hello');
复制代码
然而,它们与普通的 JavaScript 函数有很大不同:

  • 你不需要导入它们 — 它们是语言的一部分
  • 它们不是值 — 你不能将它们赋值给变量或作为参数传递给函数
  • 就像 JavaScript 关键字一样,它们只在特定位置有效(如果你把它们放在错误的地方,编译器会提示你)
[!LEGACY]
在 Svelte 5 之前,符文是不存在的。
$state

$state

$state 符文允许你创建响应式状态,这意味着当状态改变时,你的 UI 会作出响应。
  1. <button onclick={() => count++}>
  2.         点击次数: {count}
  3. </button>
复制代码
与你可能遇到的其他框架不同,这里没有用于操作状态的 API —— count 只是一个数字,而不是对象或函数,你可以像更新任何其他变量一样更新它。
深层状态

如果 $state 用于数组或简单对象,结果将是一个深度响应式的状态代理。代理(Proxies)允许 Svelte 在你读取或写入属性时运行代码,包括通过像 array.push(...) 这样的方法,触发精确的更新。
[!NOTE] 像 Set 和 Map 这样的类不会被代理,但 Svelte 为这些内置类型提供了响应式实现,可以从 svelte/reactivity 导入。
状态会递归地进行代理,直到 Svelte 找到数组或简单对象以外的东西。在像这样的情况下...
  1. let todos = $state([
  2.         {
  3.                 done: false,
  4.                 text: '添加更多待办事项'
  5.         }
  6. ]);
复制代码
...修改单个待办事项的属性将触发 UI 中依赖该特定属性的任何内容的更新:
  1. let todos = [{ done: false, text: '添加更多待办事项' }];
  2. // ---cut---
  3. todos[0].done = !todos[0].done;
复制代码
如果你向数组推入一个新对象,它也会被代理:
  1. // @filename: ambient.d.ts
  2. declare global {
  3.         const todos: Array<{ done: boolean, text: string }>
  4. }
  5. // @filename: index.js
  6. // ---cut---
  7. todos.push({
  8.         done: false,
  9.         text: '吃午饭'
  10. });
复制代码
[!NOTE] 当你更新代理的属性时,原始对象不会被改变。
注意,如果你解构一个响应式值,解构后的引用不是响应式的 —— 就像普通的 JavaScript 一样,它们在解构时就被求值了::
  1. let todos = [{ done: false, text: '添加更多待办事项' }];
  2. // ---cut---
  3. let { done, text } = todos[0];
  4. // 这不会影响 `done` 的值
  5. todos[0].done = !todos[0].done;
复制代码


你也可以在类字段中使用 $state(无论是公共的还是私有的):
  1. // @errors: 7006 2554
  2. class Todo {
  3.         done = $state(false);
  4.         text = $state();
  5.         constructor(text) {
  6.                 this.text = text;
  7.         }
  8.         reset() {
  9.                 this.text = '';
  10.                 this.done = false;
  11.         }
  12. }
复制代码
[!NOTE] 编译器将 done 和 text 转换为类原型上引用私有字段的 get/set 方法。这意味着这些属性是不可枚举的。
在 JavaScript 中调用方法时,this 的值很重要。下面这种写法不会起作用,因为 reset 方法中的 this 将是  而不是 Todo:
  1. <button onclick={todo.reset}>
  2.         重置
  3. </button>
复制代码
你可以使用内联函数...
  1. todo.reset()}>+++<button onclick={todo.reset}>
  2.         重置
  3. </button>
复制代码
...或者在类定义中使用箭头函数:
  1. // @errors: 7006 2554
  2. class Todo {
  3.         done = $state(false);
  4.         text = $state();
  5.         constructor(text) {
  6.                 this.text = text;
  7.         }
  8.         +++reset = () => {+++
  9.                 this.text = '';
  10.                 this.done = false;
  11.         }
  12. }
复制代码
$state.raw

在不希望对象和数组具有深度响应性的情况下,你可以使用 $state.raw。
使用 $state.raw 声明的状态不能被改变;它只能被重新赋值。换句话说,与其给对象的属性赋值或使用数组方法如 push,不如在想要更新时完全替换对象或数组:
  1. let person = $state.raw({
  2.         name: 'Heraclitus',
  3.         age: 49
  4. });
  5. // 这将不会生效
  6. person.age += 1;
  7. // 这将生效,因为我们创建了一个新的 person
  8. person = {
  9.         name: 'Heraclitus',
  10.         age: 50
  11. };
复制代码
这可以提高性能,特别是对于那些你本来就不打算改变的大型数组和对象,因为它避免了使它们变成响应式的开销。注意,原始状态可以包含响应式状态(例如,一个包含响应式对象的原始数组)。
$state.snapshot

要获取深度响应式 $state 代理的静态快照,使用 $state.snapshot:
  1. [/code]当你想要将某些状态传递给不希望接收代理的外部库或 API(如 structuredClone)时,这会很有用。
  2. [size=4]将状态传递给函数[/size]
  3. JavaScript 是一种按值传递的语言 —— 当你调用一个函数时,参数是值而不是变量。换句话说:
  4. [code]/// file: index.js
  5. // @filename: index.js
  6. // ---cut---
  7. /**
  8. * @param {number} a
  9. * @param {number} b
  10. */
  11. function add(a, b) {
  12.         return a + b;
  13. }
  14. let a = 1;
  15. let b = 2;
  16. let total = add(a, b);
  17. console.log(total); // 3
  18. a = 3;
  19. b = 4;
  20. console.log(total); // 仍然是 3!
复制代码
如果 add 想要访问 a 和 b 的当前值,并返回当前的 total 值,你需要使用函数:
  1. /// file: index.js
  2. // @filename: index.js
  3. // ---cut---
  4. /**
  5. * @param {() => number} getA
  6. * @param {() => number} getB
  7. */
  8. function add(+++getA, getB+++) {
  9.         return +++() => getA() + getB()+++;
  10. }
  11. let a = 1;
  12. let b = 2;
  13. let total = add+++(() => a, () => b)+++;
  14. console.log(+++total()+++); // 3
  15. a = 3;
  16. b = 4;
  17. console.log(+++total()+++); // 7
复制代码
Svelte 中的状态也不例外 —— 当你引用使用 $state 符文声明的内容时...
  1. let a = +++$state(1)+++;
  2. let b = +++$state(2)+++;
复制代码
...你访问的是它的当前值。
注意,"函数"的范围很广 —— 它包括代理的属性和 get/set 属性...
  1. /// file: index.js
  2. // @filename: index.js
  3. // ---cut---
  4. /**
  5. * @param {{ a: number, b: number }} input
  6. */
  7. function add(input) {
  8.         return {
  9.                 get value() {
  10.                         return input.a + input.b;
  11.                 }
  12.         };
  13. }
  14. let input = $state({ a: 1, b: 2 });
  15. let total = add(input);
  16. console.log(total.value); // 3
  17. input.a = 3;
  18. input.b = 4;
  19. console.log(total.value); // 7
复制代码
...不过如果你发现自己在写这样的代码,考虑使用类代替。
$derived

派生状态通过 $derived 符文声明:
  1. <button onclick={() => count++}>
  2.         {doubled}
  3. </button>
  4. <p>{count} 的两倍是 {doubled}</p>
复制代码
$derived(...) 内的表达式应该没有副作用。Svelte 将不允许在派生表达式内进行状态更改(例如 count++)。
与 $state 一样,你可以将类字段标记为 $derived。
[!NOTE] Svelte 组件中的代码仅在创建时执行一次。如果没有 $derived 符文,即使 count 发生变化,doubled 也会保持其原始值。
$derived.by

有时你需要创建不适合放在简短表达式中的复杂派生。在这些情况下,你可以使用 $derived.by,它接受一个函数作为参数。
  1. <button onclick={() => numbers.push(numbers.length + 1)}>
  2.         {numbers.join(' + ')} = {total}
  3. </button>
复制代码
本质上,$derived(expression) 等同于 $derived.by(() => expression)。
理解依赖关系

在 $derived 表达式(或 $derived.by 函数体)内同步读取的任何内容都被视为派生状态的依赖项。当状态发生变化时,派生将被标记为脏数据(dirty),并在下次读取时重新计算。
要使一段状态不被视为依赖项,请使用 untrack。
$effect

$effect

Effects 使你的应用程序能够 做点事情。当 Svelte 运行一个 effect 函数时,它会跟踪被访问(除非在 untrack 中访问)的状态(和派生状态),并在该状态后续发生变化时重新运行该函数。
Svelte 应用程序中的大多数 effects 是由 Svelte 本身创建的——例如,当 name 变化时,更新 hello {name}!

中的文本。
但你也可以使用 $effect 符文创建自己的 effects,当你需要将外部系统(无论是库、 元素,还是跨网络的某些东西)与 Svelte 应用程序内部的状态同步时,这非常有用。
[!NOTE] 避免过度使用 $effect!当你在 effects 中做太多工作时,代码通常会变得难以理解和维护。请参阅 何时不使用 $effect 了解替代方法。
你的 effects 在组件挂载到 DOM 之后运行,并在状态变化后的 微任务 中运行(demo):
  1. [/code]重新运行是批量处理的(即在同一时刻更改 color 和 size 不会导致两次单独的运行),并在所有 DOM 更新完成后发生。
  2. 你可以将 $effect 放在任何地方,不仅仅在组件的顶层,只要在组件初始化时调用它(或者在父 effect 处于激活状态时)。它就会与组件(或父 effect)的生命周期绑定,因此当组件卸载(或父 effect 被销毁)时,它会自行销毁。
  3. 你可以从 $effect 返回一个函数,该函数将在 effect 重新运行之前立即运行,并在它被销毁之前运行(demo)。
  4. [code]<h1>{count}</h1>
  5. <button onclick={() => (milliseconds *= 2)}>慢一点</button>
  6. <button onclick={() => (milliseconds /= 2)}>快一点</button>
复制代码
理解依赖关系

$effect 会自动获取在其函数体内 同步 读取的任何响应值($state、$derived、$props),并将它们注册为依赖关系。当这些依赖关系发生变化时,$effect 会安排重新运行。
在 await 之后或在 setTimeout 内部等情况下读取的值将不会被追踪。在这里,当 color 变化时,canvas 会重新绘制,但当 size 变化时将不会变化(demo):
  1. // @filename: index.ts
  2. declare let canvas: {
  3.         width: number;
  4.         height: number;
  5.         getContext(type: '2d', options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
  6. };
  7. declare let color: string;
  8. declare let size: number;
  9. // ---cut---
  10. $effect(() => {
  11.         const context = canvas.getContext('2d');
  12.         context.clearRect(0, 0, canvas.width, canvas.height);
  13.         // 每当 `color` 发生变化时,这段代码都会重新运行...
  14.         context.fillStyle = color;
  15.         setTimeout(() => {
  16.                 // ...但当 `size` 发生变化时却不会
  17.                 context.fillRect(0, 0, size, size);
  18.         }, 0);
  19. });
复制代码
effect 仅在它读取的对象发生变化时才重新运行,而不是在对象内部的属性发生变化时。(如果你想在开发时观察一个对象内部的变化,可以使用 $inspect。)
  1. <button onclick={() => (state.value += 1)}>
  2.         {state.value}
  3. </button>
  4. <p>{state.value} 的两倍是 {derived.value}</p>
复制代码
effect 仅依赖于它上次运行时读取的值。如果 a 为真,则对 b 的更改不会 导致该 effect 重新运行:
  1. let a = false;
  2. let b = false;
  3. // ---cut---
  4. $effect(() => {
  5.         console.log('运行中');
  6.         if (a || b) {
  7.                 console.log('在 if 块内');
  8.         }
  9. });
复制代码
$effect.pre

在极少数情况下,你可能需要在 DOM 更新 之前 运行代码。为此,我们可以使用 $effect.pre 符文:
  1.         {#each messages as message}
  2.                 <p>{message}</p>
  3.         {/each}
复制代码
除了时机不同,$effect.pre 的工作方式与 $effect 完全相同。
$effect.tracking

$effect.tracking 符文是一个高级特性,用于告知你代码是否在跟踪上下文中运行,例如 effect 或模板内部 (demo):
  1. <p>在模板中: {$effect.tracking()}</p>
复制代码
这允许你(例如)添加诸如订阅之类的内容而不会导致内存泄漏,方法是将它们放在子 effects 中。以下是一个 readable 函数,只要它在跟踪上下文中就会监听回调函数的变化:
  1. import { tick } from 'svelte';
  2. export default function readable<T>(
  3.         initial_value: T,
  4.         start: (callback: (update: (v: T) => T) => T) => () => void
  5. ) {
  6.         let value = $state(initial_value);
  7.         let subscribers = 0;
  8.         let stop: null | (() => void) = null;
  9.         return {
  10.                 get value() {
  11.                         // 如果在跟踪上下文中 ...
  12.                         if ($effect.tracking()) {
  13.                                 $effect(() => {
  14.                                         // ...且订阅者还没有
  15.                                         if (subscribers === 0) {
  16.                                                 // ...调用函数并监听变化以更新状态
  17.                                                 stop = start((fn) => (value = fn(value)));
  18.                                         }
  19.                                         subscribers++;
  20.                                         // 返回的回调在监听器取消监听时调用
  21.                                         return () => {
  22.                                                 tick().then(() => {
  23.                                                         subscribers--;
  24.                                                         // 如果是最后一个订阅者...
  25.                                                         if (subscribers === 0) {
  26.                                                                 // ...停止监听变化
  27.                                                                 stop?.();
  28.                                                                 stop = null;
  29.                                                         }
  30.                                                 });
  31.                                         };
  32.                                 });
  33.                         }
  34.                         return value;
  35.                 }
  36.         };
  37. }
复制代码
$effect.root

$effect.root 符文是一个高级特性,它创建了一个不会自动清理的非跟踪作用域。这对于需要手动控制的嵌套 effects 很有用。这个符文还允许在组件初始化阶段之外创建 effects。
  1. [/code][size=4]什么时候不应该使用 $effect[/size]
  2. 总体而言,$effect 最好被视为一种逃生舱口——适用于分析和直接 DOM 操作等场景——而不是一个应该频繁使用的工具。特别是要避免使用它来同步状态。千万不要这样做...
  3. [code]
复制代码
...请这样做:
  1. [/code][indent][!NOTE] 对于比像 count * 2 这样的简单表达式更复杂的内容,你也可以使用 $derived.by。
  2. [/indent]你可能会想用 effects 以复杂的方式将一个值链接到另一个值。以下示例展示了两个输入框:"已花费金额"和"剩余金额",它们彼此关联。如果你更新其中一个,另一个应该相应更新。不要为此使用 effects(demo):
  3. [code]<label>
  4.         <input type="range" bind:value={spent} max={total} />
  5.         {spent}/{total} 已花费
  6. </label>
  7. <label>
  8.         <input type="range" bind:value={left} max={total} />
  9.         {left}/{total} 剩余
  10. </label>
复制代码
相反,尽可能使用回调(demo):
  1. <label>
  2.         <input type="range" bind:value={spent} max={total} />
  3.         {spent}/{total} 已花费
  4. </label>
  5. <label>
  6.         <input type="range" bind:value={left} max={total} />
  7.         {left}/{total} 剩余
  8. </label>
复制代码
如果您出于任何原因需要使用绑定(例如当您想要某种"可写的 $derived"时),请考虑使用 getter 和 setter 来同步状态(demo):
  1. <label>
  2.         <input type="range" bind:value={spent} max={total} />
  3.         {spent}/{total} spent
  4. </label>
  5. <label>
  6.         <input type="range" bind:value={left.value} max={total} />
  7.         {left.value}/{total} left
  8. </label>
复制代码
如果您必须在 effect 中更新 $state 并且因为你读取和写入的是同一个 $state 而陷入无限循环,请使用 untrack。
Svelte 中文文档

本篇已收录在掘金专栏 《Svelte 中文文档》,该系列预计 40 篇。
系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!
此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog
通过文字建立交流本身就是一种缘分,欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册