跳到主要內容

介紹 Runes

重新思考「重新思考響應式」

在 2019 年,Svelte 3 將 JavaScript 轉變為響應式語言。Svelte 是一個網頁 UI 框架,它使用編譯器將宣告式元件程式碼(例如這樣...)

App
<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
<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 內部不同,會使得重構程式碼變得困難,例如,如果您需要將某些內容轉換為store,以便可以在多個地方使用它。

超越元件

有了 runes,響應式延伸到 .svelte 檔案的邊界之外。假設我們想以一種可以在元件之間重複使用的方式封裝我們的計數器邏輯。現在,您將在 .js.ts 檔案中使用自訂 store

計數器
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
<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>

這可行,但它很奇怪!我們發現,當您開始做更複雜的事情時,store API 可能會變得相當笨拙。

有了 runes,事情會變得簡單得多

counter.svelte
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.org.tw/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
<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;

	// 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) => 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(); // 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 非常快速。

更簡單的時代即將來臨

Runes 是一個附加功能,但它們使許多現有的概念過時

  • 元件頂層的 let 與其他任何地方的區別
  • export let
  • $:,及其所有相關的怪癖
  • <script><script context="module"> 之間的不同行為
  • $$props$$restProps
  • 生命週期函數(諸如 afterUpdate 之類的東西可以只是 $effect 函數)
  • store API 和 $ store 前綴(雖然不再需要 store,但它們並未被棄用)

對於已經使用 Svelte 的人來說,這是需要學習的新東西,儘管希望這些東西能讓您的 Svelte 應用程式更容易構建和維護。但新手將不需要學習所有這些東西 — 它們只會在文件中的「舊東西」部分中出現。

這僅僅是個開始。我們有一個很長的後續版本想法列表,這些版本將使 Svelte 更簡單且功能更強大。

試試看!

您還不能在生產環境中使用 Svelte 5。我們目前正在積極開發中,並且無法告訴您何時可以在您的應用程式中使用它。

但我們不想讓您懸而未決。我們創建了一個預覽網站,其中包含新功能的詳細說明和互動式遊樂場。您也可以瀏覽Svelte Discord#svelte-5-runes 頻道以了解更多資訊。我們很樂意收到您的回饋!