介绍符文
重新思考“重新思考响应式”
2019 年,Svelte 3 将 JavaScript 变成了一种响应式语言。Svelte 是一种 Web UI 框架,它使用编译器将如下所示的声明式组件代码...
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
clicks: {count}
</button>
...转换为紧密优化的 JavaScript 代码,并在状态(如 count
)发生变化时更新文档。由于编译器可以“看到” count
的引用位置,因此生成的代码效率极高,并且由于我们劫持了 let
和 =
等语法,而不是使用笨重的 API,因此您可以编写更少的代码。
我们经常收到的一种反馈是“我希望我可以用这种方式编写所有 JavaScript 代码”。当您习惯于组件内部的内容神奇地更新时,回到无聊的旧程序代码会感觉像从彩色变成了黑白。
Svelte 5 通过符文改变了这一切,符文解锁了通用、细粒度的响应式。
开始之前
即使我们正在改变底层的工作方式,Svelte 5 也应该对几乎所有人来说都是一个即插即用的替代方案。新功能是可选的——您现有的组件将继续工作。
我们还没有 Svelte 5 的发布日期。我们在这里向您展示的是一个正在开发中的版本,它可能会发生变化!
什么是符文?
rune /ro͞on/ 名词
用作神秘或魔法符号的字母或标记。
符文是影响 Svelte 编译器的符号。虽然 Svelte 今天使用 let
、=
、export
关键字和 $:
标签来表示特定含义,但符文使用函数语法来实现相同的功能以及更多功能。
例如,要声明一段响应式状态,我们可以使用 $state
符文
<script>
let count = 0;
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
clicks: {count}
</button>
乍一看,这似乎是倒退了一步——甚至可能是不 Svelte 风格的。如果 let count
默认情况下是响应式的,不是更好吗?
好吧,不是的。现实情况是,随着应用程序复杂度的增加,确定哪些值是响应式的,哪些值不是响应式的可能会变得很棘手。并且启发式方法仅适用于组件顶层的 let
声明,这可能会造成混淆。例如,如果需要将某些内容转换为存储以便在多个地方使用,则代码在 .svelte
文件中以一种方式运行,而在 .js
文件中以另一种方式运行可能会使代码重构变得困难。
超越组件
使用符文,响应式扩展到 .svelte
文件的边界之外。假设我们希望封装我们的计数器逻辑,以便可以在组件之间重用它。今天,您将在 .js
或 .ts
文件中使用自定义存储
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)
};
}
因为它实现了存储契约——返回值具有 subscribe
方法——我们可以通过在存储名称前加上 $
来引用存储值
<script>
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>
这可以工作,但它非常奇怪!我们发现,当您开始执行更复杂的操作时,存储 API 可能会变得相当笨拙。
使用符文,事情变得简单多了
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
};
}
<script>
import { createCounter } from './counter.svelte.js';
const counter = createCounter();
</script>
<button on:click={counter.increment}>
clicks: {$counter}
clicks: {counter.count}
</button>
在
.svelte
组件之外,符文只能在.svelte.js
和.svelte.ts
模块中使用。
请注意,我们正在返回的对象中使用get 属性,以便 counter.count
始终引用当前值,而不是函数被调用时的值。
运行时响应式
今天,Svelte 使用编译时响应式。这意味着如果您有一些使用 $:
标签在依赖项更改时自动重新运行的代码,则这些依赖项是在 Svelte 编译您的组件时确定的
<script>
export let width;
export let height;
// the compiler knows it should recalculate `area`
// when either `width` or `height` change...
$: area = width * height;
// ...and that it should log the value of `area`
// when _it_ changes
$: 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
符文,它们在表达式求值时确定表达式的依赖项
<script>
let { width, height } = $props(); // instead of `export let`
const area = $derived(width * height);
$effect(() => {
console.log(area);
});
</script>
与 $state
一样,$derived
和 $effect
也可以在您的 .js
和 .ts
文件中使用。
信号增强
像其他每个框架一样,我们意识到Knockout从一开始就对了。
Svelte 5 的响应式由信号提供支持,信号本质上是Knockout 在 2010 年所做的事情。最近,信号已由Solid推广并被众多其他框架采用。
不过,我们的做法略有不同。在 Svelte 5 中,信号是底层实现细节,而不是您直接交互的内容。因此,我们没有相同的 API 设计约束,并且可以最大限度地提高效率和人体工程学。例如,我们避免了通过函数调用访问值时出现的类型缩小问题,并且在服务器端渲染模式下编译时,我们可以完全放弃信号,因为在服务器上,它们只是开销。
信号解锁了细粒度的响应式,这意味着(例如)大型列表内值的更改无需使列表的所有其他成员失效。因此,Svelte 5 速度极快。
更简单的时代即将到来
符文是一个附加功能,但它们使许多现有的概念过时了
- 组件顶层的
let
与其他地方的let
之间的区别 export let
$:
及其所有伴随的怪癖<script>
和<script context="module">
之间不同的行为$$props
和$$restProps
- 生命周期函数(例如
afterUpdate
可以只是$effect
函数) - 存储 API 和
$
存储前缀(虽然存储不再是必需的,但它们没有被弃用)
对于那些已经使用 Svelte 的人来说,这是需要学习的新内容,尽管希望这些内容可以使您的 Svelte 应用程序更易于构建和维护。但新手无需学习所有这些内容——它只会出现在文档标题为“旧内容”的部分中。
但这仅仅是开始。我们有一个很长的想法列表,用于后续版本,这些版本将使 Svelte 更简单、更强大。
试试看!
您还不能在生产环境中使用 Svelte 5。我们目前正处于开发阶段,无法告诉您它何时可以用于您的应用程序。
但我们不想让您失望。我们创建了一个预览站点,其中包含对新功能的详细解释和交互式游乐场。您还可以访问Svelte Discord的 #svelte-5-runes
频道以了解更多信息。我们非常乐意收到您的反馈!