前言
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] 像Set
和Map
这样的类不会被代理,但 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] 编译器将done
和text
转换为类原型上引用私有字段的get
/set
方法。这意味着这些属性是不可枚举的。
在 JavaScript 中调用方法时,this
的值很重要。下面这种写法不会起作用,因为 reset
方法中的 this
将是 而不是
Todo
:
你可以使用内联函数...
...或者在类定义中使用箭头函数:
// @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()+++); // 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):
重新运行是批量处理的(即在同一时刻更改 color
和 size
不会导致两次单独的运行),并在所有 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
通过文字建立交流本身就是一种缘分,欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。