写点什么

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

作者:冴羽
  • 2025-01-22
    浙江
  • 本文字数:8131 字

    阅读完需:约 27 分钟

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


<script>  let count = $state(0);</script>
<button onclick={() => count++}> 点击次数: {count}</button>
复制代码


与你可能遇到的其他框架不同,这里没有用于操作状态的 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.tsdeclare 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 2554class 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 将是 <button> 而不是 Todo


<button onclick={todo.reset}>  重置</button>
复制代码


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


<button onclick=+++{() => todo.reset()}>+++  重置</button>
复制代码


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


// @errors: 7006 2554class 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;
// 这将生效,因为我们创建了一个新的 personperson = { name: 'Heraclitus', age: 50};
复制代码


这可以提高性能,特别是对于那些你本来就不打算改变的大型数组和对象,因为它避免了使它们变成响应式的开销。注意,原始状态可以包含响应式状态(例如,一个包含响应式对象的原始数组)。

$state.snapshot

要获取深度响应式 $state 代理的静态快照,使用 $state.snapshot


<script>  let counter = $state({ count: 0 });
function onclick() { // 将输出 `{ count: ... }` 而不是 `Proxy { ... }` console.log($state.snapshot(counter)); }</script>
复制代码


当你想要将某些状态传递给不希望接收代理的外部库或 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 符文声明:


<script>  let count = $state(0);  let doubled = $derived(count * 2);</script>
<button onclick={() => count++}> {doubled}</button>
<p>{count} 的两倍是 {doubled}</p>
复制代码


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


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


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

$derived.by

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


<script>  let numbers = $state([1, 2, 3]);  let total = $derived.by(() => {    let total = 0;    for (const n of numbers) {      total += n;    }    return total;  });</script>
<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 变化时,更新 <h1>hello {name}!</h1> 中的文本。


但你也可以使用 $effect 符文创建自己的 effects,当你需要将外部系统(无论是库、<canvas> 元素,还是跨网络的某些东西)与 Svelte 应用程序内部的状态同步时,这非常有用。


[!NOTE] 避免过度使用 $effect!当你在 effects 中做太多工作时,代码通常会变得难以理解和维护。请参阅 何时不使用 $effect 了解替代方法。


你的 effects 在组件挂载到 DOM 之后运行,并在状态变化后的 微任务 中运行(demo):


<script>  let size = $state(50);  let color = $state('#ff3e00');
let canvas;
$effect(() => { const context = canvas.getContext('2d'); context.clearRect(0, 0, canvas.width, canvas.height);
// 只要 `color` 或 `size` 发生变化,这段代码就会重新执行 context.fillStyle = color; context.fillRect(0, 0, size, size); });</script>
<canvas bind:this={canvas} width="100" height="100" />
复制代码


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


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


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


<script>  let count = $state(0);  let milliseconds = $state(1000);
$effect(() => { // 每当 `milliseconds` 变化时,这段代码都会被重新创建 const interval = setInterval(() => { count += 1; }, milliseconds);
return () => { // 如果提供了回调,它将在 // a) effect 重新运行之前立即被调用 // b) 当组件被销毁时被调用 clearInterval(interval); }; });</script>
<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.tsdeclare 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。)


<script>  let state = $state({ value: 0 });  let derived = $derived({ value: state.value * 2 });
// 这只会运行一次,因为 `state` 从未被重新分配(仅被修改) $effect(() => { state; });
// 这将在每当 `state.value` 变化时运行... $effect(() => { state.value; });
// ...这一点也是如此,因为 `derived` 每次都是一个新对象 $effect(() => { derived; });</script>
<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 符文:


<script>  import { tick } from 'svelte';
let div = $state(); let messages = $state([]);
// ...
$effect.pre(() => { if (!div) return; // 尚未挂载
// 引用 `messages` 数组长度,以便当它改变时,此代码重新运行 messages.length;
// 当新消息被添加时自动滚动 if (div.offsetHeight + div.scrollTop > div.scrollHeight - 20) { tick().then(() => { div.scrollTo(0, div.scrollHeight); }); } });</script>
<div bind:this={div}> {#each messages as message} <p>{message}</p> {/each}</div>
复制代码


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

$effect.tracking

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


<script>  console.log('在组件设置中:', $effect.tracking()); // false
$effect(() => { console.log('在效果中:', $effect.tracking()); // true });</script>
<p>在模板中: {$effect.tracking()}</p> <!-- true -->
复制代码


这允许你(例如)添加诸如订阅之类的内容而不会导致内存泄漏,方法是将它们放在子 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。


<script>  let count = $state(0);
const cleanup = $effect.root(() => { $effect(() => { console.log(count); });
return () => { console.log('effect root cleanup'); }; });</script>
复制代码

什么时候不应该使用 $effect

总体而言,$effect 最好被视为一种逃生舱口——适用于分析和直接 DOM 操作等场景——而不是一个应该频繁使用的工具。特别是要避免使用它来同步状态。千万不要这样做...


<script>  let count = $state(0);  let doubled = $state();
// 不要这样做! $effect(() => { doubled = count * 2; });</script>
复制代码


...请这样做:


<script>  let count = $state(0);  let doubled = $derived(count * 2);</script>
复制代码


[!NOTE] 对于比像 count * 2 这样的简单表达式更复杂的内容,你也可以使用 $derived.by


你可能会想用 effects 以复杂的方式将一个值链接到另一个值。以下示例展示了两个输入框:"已花费金额"和"剩余金额",它们彼此关联。如果你更新其中一个,另一个应该相应更新。不要为此使用 effects(demo):


<script>  let total = 100;  let spent = $state(0);  let left = $state(total);
$effect(() => { left = total - spent; });
$effect(() => { spent = total - left; });</script>
<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):


<script>  let total = 100;  let spent = $state(0);  let left = $state(total);
function updateSpent(e) { spent = +e.target.value; left = total - spent; }
function updateLeft(e) { left = +e.target.value; spent = total - left; }</script>
<label> <input type="range" value={spent} oninput={updateSpent} max={total} /> {spent}/{total} 已花费</label>
<label> <input type="range" value={left} oninput={updateLeft} max={total} /> {left}/{total} 剩余</label>
复制代码


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


<script>  let total = 100;  let spent = $state(0);
let left = { get value() { return total - spent; }, set value(v) { spent = total - v; } };</script>
<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


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

发布于: 刚刚阅读数: 4
用户头像

冴羽

关注

还未添加个人签名 2019-01-25 加入

分享前端技术。GitHub 中国区 Top 30,Blog 30K Star。至今写过 14 个系列文章,全网千万阅读,主页地址:yayujs.卡木

评论

发布
暂无评论
Svelte 最新中文文档翻译(3)—— 符文(Runes)上_vue.js_冴羽_InfoQ写作社区