桂册 发表于 2025-6-6 10:06:52

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

前言

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

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

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

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

$state

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

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

// @filename: index.js
// ---cut---
todos.push({
        done: false,
        text: '吃午饭'
});[!NOTE] 当你更新代理的属性时,原始对象不会被改变。
注意,如果你解构一个响应式值,解构后的引用不是响应式的 —— 就像普通的 JavaScript 一样,它们在解构时就被求值了::
let todos = [{ done: false, text: '添加更多待办事项' }];
// ---cut---
let { done, text } = todos;

// 这不会影响 `done` 的值
todos.done = !todos.done;类

你也可以在类字段中使用 $state(无论是公共的还是私有的):
// @errors: 7006 2554
class Todo {
        done = $state(false);
        text = $state();

        constructor(text) {
                this.text = text;
        }

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

        constructor(text) {
                this.text = text;
        }

        +++reset = () => {+++
                this.text = '';
                this.done = false;
        }
}$state.raw

在不希望对象和数组具有深度响应性的情况下,你可以使用 $state.raw。
使用 $state.raw 声明的状态不能被改变;它只能被重新赋值。换句话说,与其给对象的属性赋值或使用数组方法如 push,不如在想要更新时完全替换对象或数组:
let person = $state.raw({
        name: 'Heraclitus',
        age: 49
});

// 这将不会生效
person.age += 1;

// 这将生效,因为我们创建了一个新的 person
person = {
        name: 'Heraclitus',
        age: 50
};这可以提高性能,特别是对于那些你本来就不打算改变的大型数组和对象,因为它避免了使它们变成响应式的开销。注意,原始状态可以包含响应式状态(例如,一个包含响应式对象的原始数组)。
$state.snapshot

要获取深度响应式 $state 代理的静态快照,使用 $state.snapshot:
当你想要将某些状态传递给不希望接收代理的外部库或 API(如 structuredClone)时,这会很有用。
将状态传递给函数

JavaScript 是一种按值传递的语言 —— 当你调用一个函数时,参数是值而不是变量。换句话说:
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {number} a
* @param {number} b
*/
function add(a, b) {
        return a + b;
}

let a = 1;
let b = 2;
let total = add(a, b);
console.log(total); // 3

a = 3;
b = 4;
console.log(total); // 仍然是 3!如果 add 想要访问 a 和 b 的当前值,并返回当前的 total 值,你需要使用函数:
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {() => number} getA
* @param {() => number} getB
*/
function add(+++getA, getB+++) {
        return +++() => getA() + getB()+++;
}

let a = 1;
let b = 2;
let total = add+++(() => a, () => b)+++;
console.log(+++total()+++); // 3

a = 3;
b = 4;
console.log(+++total()+++); // 7Svelte 中的状态也不例外 —— 当你引用使用 $state 符文声明的内容时...
let a = +++$state(1)+++;
let b = +++$state(2)+++;...你访问的是它的当前值。
注意,"函数"的范围很广 —— 它包括代理的属性和 get/set 属性...
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {{ a: number, b: number }} input
*/
function add(input) {
        return {
                get value() {
                        return input.a + input.b;
                }
        };
}

let input = $state({ a: 1, b: 2 });
let total = add(input);
console.log(total.value); // 3

input.a = 3;
input.b = 4;
console.log(total.value); // 7...不过如果你发现自己在写这样的代码,考虑使用类代替。
$derived

派生状态通过 $derived 符文声明:
<button onclick={() => count++}>
        {doubled}
</button>

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

有时你需要创建不适合放在简短表达式中的复杂派生。在这些情况下,你可以使用 $derived.by,它接受一个函数作为参数。
<button onclick={() => numbers.push(numbers.length + 1)}>
        {numbers.join(' + ')} = {total}
</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):
重新运行是批量处理的(即在同一时刻更改 color 和 size 不会导致两次单独的运行),并在所有 DOM 更新完成后发生。
你可以将 $effect 放在任何地方,不仅仅在组件的顶层,只要在组件初始化时调用它(或者在父 effect 处于激活状态时)。它就会与组件(或父 effect)的生命周期绑定,因此当组件卸载(或父 effect 被销毁)时,它会自行销毁。
你可以从 $effect 返回一个函数,该函数将在 effect 重新运行之前立即运行,并在它被销毁之前运行(demo)。
<h1>{count}</h1>

<button onclick={() => (milliseconds *= 2)}>慢一点</button>
<button onclick={() => (milliseconds /= 2)}>快一点</button>理解依赖关系

$effect 会自动获取在其函数体内 同步 读取的任何响应值($state、$derived、$props),并将它们注册为依赖关系。当这些依赖关系发生变化时,$effect 会安排重新运行。
在 await 之后或在 setTimeout 内部等情况下读取的值将不会被追踪。在这里,当 color 变化时,canvas 会重新绘制,但当 size 变化时将不会变化(demo):
// @filename: index.ts
declare let canvas: {
        width: number;
        height: number;
        getContext(type: '2d', options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
};
declare let color: string;
declare let size: number;

// ---cut---
$effect(() => {
        const context = canvas.getContext('2d');
        context.clearRect(0, 0, canvas.width, canvas.height);

        // 每当 `color` 发生变化时,这段代码都会重新运行...
        context.fillStyle = color;

        setTimeout(() => {
                // ...但当 `size` 发生变化时却不会
                context.fillRect(0, 0, size, size);
        }, 0);
});effect 仅在它读取的对象发生变化时才重新运行,而不是在对象内部的属性发生变化时。(如果你想在开发时观察一个对象内部的变化,可以使用 $inspect。)
<button onclick={() => (state.value += 1)}>
        {state.value}
</button>

<p>{state.value} 的两倍是 {derived.value}</p>effect 仅依赖于它上次运行时读取的值。如果 a 为真,则对 b 的更改不会 导致该 effect 重新运行:
let a = false;
let b = false;
// ---cut---
$effect(() => {
        console.log('运行中');

        if (a || b) {
                console.log('在 if 块内');
        }
});$effect.pre

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

$effect.tracking 符文是一个高级特性,用于告知你代码是否在跟踪上下文中运行,例如 effect 或模板内部 (demo):
<p>在模板中: {$effect.tracking()}</p> 这允许你(例如)添加诸如订阅之类的内容而不会导致内存泄漏,方法是将它们放在子 effects 中。以下是一个 readable 函数,只要它在跟踪上下文中就会监听回调函数的变化:
import { tick } from 'svelte';

export default function readable<T>(
        initial_value: T,
        start: (callback: (update: (v: T) => T) => T) => () => void
) {
        let value = $state(initial_value);

        let subscribers = 0;
        let stop: null | (() => void) = null;

        return {
                get value() {
                        // 如果在跟踪上下文中 ...
                        if ($effect.tracking()) {
                                $effect(() => {
                                        // ...且订阅者还没有
                                        if (subscribers === 0) {
                                                // ...调用函数并监听变化以更新状态
                                                stop = start((fn) => (value = fn(value)));
                                        }

                                        subscribers++;

                                        // 返回的回调在监听器取消监听时调用
                                        return () => {
                                                tick().then(() => {
                                                        subscribers--;
                                                        // 如果是最后一个订阅者...
                                                        if (subscribers === 0) {
                                                                // ...停止监听变化
                                                                stop?.();
                                                                stop = null;
                                                        }
                                                });
                                        };
                                });
                        }

                        return value;
                }
        };
}$effect.root

$effect.root 符文是一个高级特性,它创建了一个不会自动清理的非跟踪作用域。这对于需要手动控制的嵌套 effects 很有用。这个符文还允许在组件初始化阶段之外创建 effects。
什么时候不应该使用 $effect

总体而言,$effect 最好被视为一种逃生舱口——适用于分析和直接 DOM 操作等场景——而不是一个应该频繁使用的工具。特别是要避免使用它来同步状态。千万不要这样做...
...请这样做:
[!NOTE] 对于比像 count * 2 这样的简单表达式更复杂的内容,你也可以使用 $derived.by。
你可能会想用 effects 以复杂的方式将一个值链接到另一个值。以下示例展示了两个输入框:"已花费金额"和"剩余金额",它们彼此关联。如果你更新其中一个,另一个应该相应更新。不要为此使用 effects(demo):
<label>
        <input type="range" bind:value={spent} max={total} />
        {spent}/{total} 已花费
</label>

<label>
        <input type="range" bind:value={left} max={total} />
        {left}/{total} 剩余
</label>相反,尽可能使用回调(demo):
<label>
        <input type="range" bind:value={spent} max={total} />
        {spent}/{total} 已花费
</label>

<label>
        <input type="range" bind:value={left} max={total} />
        {left}/{total} 剩余
</label>如果您出于任何原因需要使用绑定(例如当您想要某种"可写的 $derived"时),请考虑使用 getter 和 setter 来同步状态(demo):
<label>
        <input type="range" bind:value={spent} max={total} />
        {spent}/{total} spent
</label>

<label>
        <input type="range" bind:value={left.value} max={total} />
        {left.value}/{total} left
</label>如果您必须在 effect 中更新 $state 并且因为你读取和写入的是同一个 $state 而陷入无限循环,请使用 untrack。
Svelte 中文文档

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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: Svelte 最新中文文档翻译(3)—— 符文(Runes)上