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 会作出响应。



与你可能遇到的其他框架不同,这里没有用于操作状态的 API —— count 只是一个数字,而不是对象或函数,你可以像更新任何其他变量一样更新它。

深层状态

如果 $state 用于数组或简单对象,结果将是一个深度响应式的状态代理代理(Proxies)允许 Svelte 在你读取或写入属性时运行代码,包括通过像 array.push(...) 这样的方法,触发精确的更新。

[!NOTE] 像 SetMap 这样的类不会被代理,但 Svelte 为这些内置类型提供了响应式实现,可以从 svelte/reactivity 导入。

状态会递归地进行代理,直到 Svelte 找到数组或简单对象以外的东西。在像这样的情况下...

let todos = $state([
    {
        done: false,
        text: '添加更多待办事项'
    }
]);

...修改单个待办事项的属性将触发 UI 中依赖该特定属性的任何内容的更新:

let todos = [{ done: false, text: '添加更多待办事项' }];
// ---cut---
todos[0].done = !todos[0].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[0];

// 这不会影响 `done` 的值
todos[0].done = !todos[0].done;

你也可以在类字段中使用 $state(无论是公共的还是私有的):

// @errors: 7006 2554
class Todo {
    done = $state(false);
    text = $state();

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

    reset() {
        this.text = '';
        this.done = false;
    }
}
[!NOTE] 编译器将 donetext 转换为类原型上引用私有字段的 get/set 方法。这意味着这些属性是不可枚举的。

在 JavaScript 中调用方法时,this 的值很重要。下面这种写法不会起作用,因为 reset 方法中的 this 将是

你可以使用内联函数...

...或者在类定义中使用箭头函数:

// @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 想要访问 ab 的当前值,并返回当前的 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()+++); // 7

Svelte 中的状态也不例外 —— 当你引用使用 $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 符文声明:





{count} 的两倍是 {doubled}

$derived(...) 内的表达式应该没有副作用。Svelte 将不允许在派生表达式内进行状态更改(例如 count++)。

$state 一样,你可以将类字段标记为 $derived

[!NOTE] Svelte 组件中的代码仅在创建时执行一次。如果没有 $derived 符文,即使 count 发生变化,doubled 也会保持其原始值。

$derived.by

有时你需要创建不适合放在简短表达式中的复杂派生。在这些情况下,你可以使用 $derived.by,它接受一个函数作为参数。



本质上,$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):



重新运行是批量处理的(即在同一时刻更改 colorsize 不会导致两次单独的运行),并在所有 DOM 更新完成后发生。

你可以将 $effect 放在任何地方,不仅仅在组件的顶层,只要在组件初始化时调用它(或者在父 effect 处于激活状态时)。它就会与组件(或父 effect)的生命周期绑定,因此当组件卸载(或父 effect 被销毁)时,它会自行销毁。

你可以从 $effect 返回一个函数,该函数将在 effect 重新运行之前立即运行,并在它被销毁之前运行(demo)。



{count}

理解依赖关系

$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。)





{state.value} 的两倍是 {derived.value}

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}

{message}

{/each}

除了时机不同,$effect.pre 的工作方式与 $effect 完全相同。

$effect.tracking

$effect.tracking 符文是一个高级特性,用于告知你代码是否在跟踪上下文中运行,例如 effect 或模板内部 (demo):



在模板中: {$effect.tracking()}

这允许你(例如)添加诸如订阅之类的内容而不会导致内存泄漏,方法是将它们放在子 effects 中。以下是一个 readable 函数,只要它在跟踪上下文中就会监听回调函数的变化:

import { tick } from 'svelte';

export default function readable(
    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):





相反,尽可能使用回调(demo):





如果您出于任何原因需要使用绑定(例如当您想要某种"可写的 $derived"时),请考虑使用 getter 和 setter 来同步状态(demo):





如果您必须在 effect 中更新 $state 并且因为你读取和写入的是同一个 $state 而陷入无限循环,请使用 untrack

Svelte 中文文档

本篇已收录在掘金专栏 《Svelte 中文文档》,该系列预计 40 篇。

系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

此外我还写过 JavaScript 系列TypeScript 系列React 系列Next.js 系列冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog

通过文字建立交流本身就是一种缘分,欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。

你可能感兴趣的:(Svelte 最新官方文档翻译(3)—— 符文(Runes)上)