介绍 runes
重新思考“重新思考响应式”
2019年,Svelte 3 将 JavaScript 转变为一种响应式语言。Svelte 是一个 Web UI 框架,它使用编译器将声明式组件代码(比如以下代码)...
<! 文件: App.svelte >
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
clicks: {count}
</button>
...编译成高度优化的 JavaScript,当像 count
这样的状态发生更改时更新文档。因为编译器可以“看到”count
的引用位置,生成的代码效率极高,而且因为我们劫持了 let
和 =
等语法,而不是使用繁琐的 API,你可以写更少的代码。
我们经常收到一个共同的反馈:“我希望可以把所有的 JavaScript 都这样写。” 习惯了组件内部自动更新的事情,回到枯燥的旧的程序式代码,就像从彩色世界回到黑白电影。
Svelte 5 带来了 runes 这一特性,实现了通用、细粒度的响应式编程。
在开始之前
尽管我们正在改变底层的工作方式,但 Svelte 5 对于绝大部分人来说应该是可直接替换的升级。这些新功能是可选的——你现有的组件将继续正常运行。
我们还没有 Svelte 5 的发布日期。这里展示的内容是尚在开发中的功能,可能会发生变化!
什么是 runes?
rune /ro͞on/ 名词
一种用作神秘或魔法符号的字母或标记。
Runes 是影响 Svelte 编译器的符号。当前 Svelte 使用 let
、=
、export
关键字和 $:
标签代表特定含义,而 runes 使用函数语法来实现同样的功能,甚至更多。
例如,要声明一段响应式状态,我们可以使用 $state
rune:
<! 文件: App.svelte >
<script>
let count = 0;
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
clicks: {count}
</button>
乍看之下,这似乎是一个退步——甚至可能被认为是不符合 Svelte 的风格。let count
默认就是响应式的不是更好吗?
其实不然。现实是,随着应用复杂性的增加,分辨哪些值是响应式的、哪些不是会变得很棘手。而这个规则仅适用于组件顶层的 let
声明,这可能会引起混淆。例如,当你将代码重构为store以便在多个位置使用时,.svelte
和 .js
中的代码表现不同会使得代码难以重构。
超越组件
通过 runes,响应式功能扩展到了 .svelte
文件的边界之外。假设我们希望将计数器逻辑封装,以便在多个组件之间重用。今天,你可能会在 .js
或 .ts
文件中创建一个自定义 store:
/// 文件: counter.js
import { function writable<T>(value?: T | undefined, start?: StartStopNotifier<T> | undefined): Writable<T>
Create a Writable
store that allows both updating and reading by subscription.
writable } from 'svelte/store';
export function function createCounter(): {
subscribe: (this: void, run: Subscriber<number>, invalidate?: () => void) => Unsubscriber;
increment: () => void;
}
createCounter() {
const { const subscribe: (this: void, run: Subscriber<number>, invalidate?: () => void) => Unsubscriber
Subscribe on value changes.
subscribe, const update: (this: void, updater: Updater<number>) => void
Update value using callback and inform subscribers.
update } = writable<number>(value?: number | undefined, start?: StartStopNotifier<number> | undefined): Writable<number>
Create a Writable
store that allows both updating and reading by subscription.
writable(0);
return {
subscribe: (this: void, run: Subscriber<number>, invalidate?: () => void) => Unsubscriber
subscribe,
increment: () => void
increment: () => const update: (this: void, updater: Updater<number>) => void
Update value using callback and inform subscribers.
update((n: number
n) => n: number
n + 1)
};
}
因为这是一个实现了 store 合约 的对象(返回值有一个 subscribe
方法),我们可以通过在 store 名称前加 $
来引用 store 值:
<! 文件: App.svelte >
<script>
/// 文件: App.svelte
import { createCounter } from './counter.js';
const counter = createCounter();
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
clicks: {count}
<button on:click={counter.increment}>
clicks: {$counter}
</button>
这确实有效,但感觉有点古怪!我们发现,当你开始执行更复杂的操作时,store 的 API 会变得相当难以处理。
使用 runes,事情变得更简单了:
/// 文件: counter.svelte.js
import { writable } from 'svelte/store';
export function function createCounter(): {
readonly count: number;
increment: () => number;
}
createCounter() {
const { subscribe, update } = writable(0);
let let count: number
count = function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
$state(0);
return {
subscribe,
increment: () => update((n) => n + 1)
get count: number
count() { return let count: number
count },
increment: () => number
increment: () => let count: number
count += 1
};
}<! 文件: App.svelte >
<script>
import { createCounter } from './counter.svelte.js';
const counter = createCounter();
</script>
<button on:click={counter.increment}>
clicks: {$counter}
clicks: {counter.count}
</button>
在
.svelte
组件之外,runes 仅能用于.svelte.js
和.svelte.ts
模块。
请注意,我们在返回的对象中使用了get 属性,这样 counter.count
始终引用当前的值,而不是函数调用时的值。
运行时响应式
目前,Svelte 使用 编译时响应式。也就是说,如果你使用 $:
标签来在依赖项更改时自动重新执行某些代码,那么这些依赖项是在 Svelte 编译组件时决定的:
<script>
export let width;
export let height;
// 编译器知道当 `width` 或 `height` 更改时需要重新计算 `area`
$: area = width * height;
// 并且当 `area` 更改时,重新记录它的值
$: console.log(area);
</script>
这种方法很好用……直到不够用为止。例如,如果我们重构上面的代码:
const const multiplyByHeight: (width: any) => number
multiplyByHeight = (width) => width: any
width * height;
$: area = const multiplyByHeight: (width: any) => number
multiplyByHeight(width);
因为 $: area = ...
声明只能“看到” width
,所以它不会在 height
更改时重新计算。结果就是,代码难以重构,且了解 Svelte 何时更新哪些值的背后规律会变得很复杂。
Svelte 5 引入了 $derived
和 $effect
runes,它们可以在表达式评估时决定依赖项:
<script>
let { width, height } = $props(); // 替代 `export let`
const area = $derived(width * height);
$effect(() => {
console.log(area);
});
</script>
与 $state
一样,$derived
和 $effect
也可以在 .js
和 .ts
文件中使用。
信号增强
和其他框架一样,我们也渐渐意识到 Knockout 早在十几年前就证明了它的正确性。
Svelte 5 的响应式由 signals 驱动,这基本上就是Knockout 在 2010 年所做的。最近,signals 被 Solid 等框架广泛推广,并被许多其他框架采纳。
不过我们做得略有不同。在 Svelte 5 中,signals 是底层的实现细节,而不是需要直接操作的内容。因此,我们不受 API 设计约束的限制,可以最大化效率和易用性。例如,我们可以避免因值通过函数调用访问而导致的类型缩窄问题,并且在服务器端渲染模式下编译时可以完全去除 signals,因为在服务器上它们只会增加负担。
Signals 启用了 细粒度响应式,这意味着(例如)大列表中的某个值发生变化时不需要使列表中的_其他_成员失效。因此,Svelte 5 的性能极为卓越。
更简单的未来
Runes 是一个新增功能,但它让许多现有概念变得多余:
- 组件顶层的
let
和其他地方的let
之间的差异 export let
$:
及其相关的各种问题<script>
和<script context="module">
之间的不同行为$$props
和$$restProps
- 生命周期函数(诸如
afterUpdate
之类的功能可以直接用$effect
函数实现) - store API 和
$
store 前缀(尽管 stores 不再是必要的,但不会被废弃)
对于已经在使用 Svelte 的用户来说,这意味着需要学习一些新东西,但这些新功能有望让你的 Svelte 应用更容易构建和维护。而对于新用户来说,他们无需再学习这些旧概念了——它们将被记录在文档的“旧的处理方式”一节中。
不过,这仅仅是个开始。我们有一个关于后续版本的长长的想法清单,这些版本将使 Svelte 更加简单且功能更强大。
试试看!
你还不能在生产环境中使用 Svelte 5。我们目前正埋头开发,尚无法告诉你它何时可以在你的应用中使用。
但我们不想让你干等着。我们创建了一个预览站点,其中包含新功能的详细解释和交互式演示。你还可以访问 Svelte Discord 的 #svelte-5-runes
频道了解更多内容。我们期待你的反馈!