Skip to main content

介绍 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 这一特性,实现了通用、细粒度的响应式编程

介绍 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.

@paramvalue initial value
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.

@paramvalue initial value
writable
(0);
return { subscribe: (this: void, run: Subscriber<number>, invalidate?: () => void) => Unsubscribersubscribe, increment: () => voidincrement: () => const update: (this: void, updater: Updater<number>) => void

Update value using callback and inform subscribers.

@paramupdater callback
update
((n: numbern) => n: numbern + 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: numbercount =
function $state<0>(initial: 0): 0 (+1 overload)
namespace $state

Declares reactive state.

Example:

let count = $state(0);

https://svelte.dev/docs/svelte/$state

@paraminitial The initial value
$state
(0);
return { subscribe, increment: () => update((n) => n + 1) get count: numbercount() { return let count: numbercount }, increment: () => numberincrement: () => let count: numbercount += 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) => numbermultiplyByHeight = (width) => width: anywidth * height;

$: area = const multiplyByHeight: (width: any) => numbermultiplyByHeight(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 频道了解更多内容。我们期待你的反馈!