Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Svelte 最新中文文档翻译(3)—— 符文(Runes)上 #331

Open
mqyqingfeng opened this issue Jan 22, 2025 · 0 comments
Open

Comments

@mqyqingfeng
Copy link
Owner

前言

Svelte,一个非常“有趣”、用起来“很爽”的前端框架。从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1

Image

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 会作出响应。

<script>
	let count = $state(0);
</script>

<button onclick={() => count++}>
	点击次数: {count}
</button>

与你可能遇到的其他框架不同,这里没有用于操作状态的 API —— count 只是一个数字,而不是对象或函数,你可以像更新任何其他变量一样更新它。

深层状态

如果 $state 用于数组或简单对象,结果将是一个深度响应式的状态代理代理(Proxies)允许 Svelte 在你读取或写入属性时运行代码,包括通过像 array.push(...) 这样的方法,触发精确的更新。

[!NOTE] 像 SetMap 这样的类不会被代理,但 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] 编译器将 donetext 转换为类原型上引用私有字段的 get/set 方法。这意味着这些属性是不可枚举的。

在 JavaScript 中调用方法时,this 的值很重要。下面这种写法不会起作用,因为 reset 方法中的 this 将是 <button> 而不是 Todo

<button onclick={todo.reset}>
	重置
</button>

你可以使用内联函数...

<button onclick=+++{() => todo.reset()}>+++
	重置
</button>

...或者在类定义中使用箭头函数:

// @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

<script>
	let counter = $state({ count: 0 });

	function onclick() {
		// 将输出 `{ count: ... }` 而不是 `Proxy { ... }`
		console.log($state.snapshot(counter));
	}
</script>

当你想要将某些状态传递给不希望接收代理的外部库或 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 想要访问 ab 的当前值,并返回当前的 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 符文声明:

<script>
	let count = $state(0);
	let doubled = $derived(count * 2);
</script>

<button onclick={() => count++}>
	{doubled}
</button>

<p>{count} 的两倍是 {doubled}</p>

$derived(...) 内的表达式应该没有副作用。Svelte 将不允许在派生表达式内进行状态更改(例如 count++)。

$state 一样,你可以将类字段标记为 $derived

[!NOTE] Svelte 组件中的代码仅在创建时执行一次。如果没有 $derived 符文,即使 count 发生变化,doubled 也会保持其原始值。

$derived.by

有时你需要创建不适合放在简短表达式中的复杂派生。在这些情况下,你可以使用 $derived.by,它接受一个函数作为参数。

<script>
	let numbers = $state([1, 2, 3]);
	let total = $derived.by(() => {
		let total = 0;
		for (const n of numbers) {
			total += n;
		}
		return total;
	});
</script>

<button onclick={() => numbers.push(numbers.length + 1)}>
	{numbers.join(' + ')} = {total}
</button>

本质上,$derived(expression) 等同于 $derived.by(() => expression)

理解依赖关系

$derived 表达式(或 $derived.by 函数体)内同步读取的任何内容都被视为派生状态的依赖项。当状态发生变化时,派生将被标记为脏数据(dirty),并在下次读取时重新计算。

要使一段状态不被视为依赖项,请使用 untrack

$effect

$effect

Effects 使你的应用程序能够 做点事情。当 Svelte 运行一个 effect 函数时,它会跟踪被访问(除非在 untrack 中访问)的状态(和派生状态),并在该状态后续发生变化时重新运行该函数。

Svelte 应用程序中的大多数 effects 是由 Svelte 本身创建的——例如,当 name 变化时,更新 <h1>hello {name}!</h1> 中的文本。

但你也可以使用 $effect 符文创建自己的 effects,当你需要将外部系统(无论是库、<canvas> 元素,还是跨网络的某些东西)与 Svelte 应用程序内部的状态同步时,这非常有用。

[!NOTE] 避免过度使用 $effect!当你在 effects 中做太多工作时,代码通常会变得难以理解和维护。请参阅 何时不使用 $effect 了解替代方法。

你的 effects 在组件挂载到 DOM 之后运行,并在状态变化后的 微任务 中运行(demo):

<script>
	let size = $state(50);
	let color = $state('#ff3e00');

	let canvas;

	$effect(() => {
		const context = canvas.getContext('2d');
		context.clearRect(0, 0, canvas.width, canvas.height);

		// 只要 `color` 或 `size` 发生变化,这段代码就会重新执行
		context.fillStyle = color;
		context.fillRect(0, 0, size, size);
	});
</script>

<canvas bind:this={canvas} width="100" height="100" />

重新运行是批量处理的(即在同一时刻更改 colorsize 不会导致两次单独的运行),并在所有 DOM 更新完成后发生。

你可以将 $effect 放在任何地方,不仅仅在组件的顶层,只要在组件初始化时调用它(或者在父 effect 处于激活状态时)。它就会与组件(或父 effect)的生命周期绑定,因此当组件卸载(或父 effect 被销毁)时,它会自行销毁。

你可以从 $effect 返回一个函数,该函数将在 effect 重新运行之前立即运行,并在它被销毁之前运行(demo)。

<script>
	let count = $state(0);
	let milliseconds = $state(1000);

	$effect(() => {
		// 每当 `milliseconds` 变化时,这段代码都会被重新创建
		const interval = setInterval(() => {
			count += 1;
		}, milliseconds);

		return () => {
			// 如果提供了回调,它将在
			// a) effect 重新运行之前立即被调用
			// b) 当组件被销毁时被调用
			clearInterval(interval);
		};
	});
</script>

<h1>{count}</h1>

<button onclick={() => (milliseconds *= 2)}>慢一点</button>
<button onclick={() => (milliseconds /= 2)}>快一点</button>

理解依赖关系

$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。)

<script>
	let state = $state({ value: 0 });
	let derived = $derived({ value: state.value * 2 });

	// 这只会运行一次,因为 `state` 从未被重新分配(仅被修改)
	$effect(() => {
		state;
	});

	// 这将在每当 `state.value` 变化时运行...
	$effect(() => {
		state.value;
	});

	// ...这一点也是如此,因为 `derived` 每次都是一个新对象
	$effect(() => {
		derived;
	});
</script>

<button onclick={() => (state.value += 1)}>
	{state.value}
</button>

<p>{state.value} 的两倍是 {derived.value}</p>

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 符文:

<script>
	import { tick } from 'svelte';

	let div = $state();
	let messages = $state([]);

	// ...

	$effect.pre(() => {
		if (!div) return; // 尚未挂载

		// 引用 `messages` 数组长度,以便当它改变时,此代码重新运行
		messages.length;

		// 当新消息被添加时自动滚动
		if (div.offsetHeight + div.scrollTop > div.scrollHeight - 20) {
			tick().then(() => {
				div.scrollTo(0, div.scrollHeight);
			});
		}
	});
</script>

<div bind:this={div}>
	{#each messages as message}
		<p>{message}</p>
	{/each}
</div>

除了时机不同,$effect.pre 的工作方式与 $effect 完全相同。

$effect.tracking

$effect.tracking 符文是一个高级特性,用于告知你代码是否在跟踪上下文中运行,例如 effect 或模板内部 (demo):

<script>
	console.log('在组件设置中:', $effect.tracking()); // false

	$effect(() => {
		console.log('在效果中:', $effect.tracking()); // true
	});
</script>

<p>在模板中: {$effect.tracking()}</p> <!-- true -->

这允许你(例如)添加诸如订阅之类的内容而不会导致内存泄漏,方法是将它们放在子 effects 中。以下是一个 readable 函数,只要它在跟踪上下文中就会监听回调函数的变化:

import { tick } from 'svelte';

export default function readable<T>(
	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。

<script>
	let count = $state(0);

	const cleanup = $effect.root(() => {
		$effect(() => {
			console.log(count);
		});

		return () => {
			console.log('effect root cleanup');
		};
	});
</script>

什么时候不应该使用 $effect

总体而言,$effect 最好被视为一种逃生舱口——适用于分析和直接 DOM 操作等场景——而不是一个应该频繁使用的工具。特别是要避免使用它来同步状态。千万不要这样做...

<script>
	let count = $state(0);
	let doubled = $state();

	// 不要这样做!
	$effect(() => {
		doubled = count * 2;
	});
</script>

...请这样做:

<script>
	let count = $state(0);
	let doubled = $derived(count * 2);
</script>

[!NOTE] 对于比像 count * 2 这样的简单表达式更复杂的内容,你也可以使用 $derived.by

你可能会想用 effects 以复杂的方式将一个值链接到另一个值。以下示例展示了两个输入框:"已花费金额"和"剩余金额",它们彼此关联。如果你更新其中一个,另一个应该相应更新。不要为此使用 effects(demo):

<script>
	let total = 100;
	let spent = $state(0);
	let left = $state(total);

	$effect(() => {
		left = total - spent;
	});

	$effect(() => {
		spent = total - left;
	});
</script>

<label>
	<input type="range" bind:value={spent} max={total} />
	{spent}/{total} 已花费
</label>

<label>
	<input type="range" bind:value={left} max={total} />
	{left}/{total} 剩余
</label>

相反,尽可能使用回调(demo):

<script>
	let total = 100;
	let spent = $state(0);
	let left = $state(total);

	function updateSpent(e) {
		spent = +e.target.value;
		left = total - spent;
	}

	function updateLeft(e) {
		left = +e.target.value;
		spent = total - left;
	}
</script>

<label>
	<input type="range" value={spent} oninput={updateSpent} max={total} />
	{spent}/{total} 已花费
</label>

<label>
	<input type="range" value={left} oninput={updateLeft} max={total} />
	{left}/{total} 剩余
</label>

如果您出于任何原因需要使用绑定(例如当您想要某种"可写的 $derived"时),请考虑使用 getter 和 setter 来同步状态(demo):

<script>
	let total = 100;
	let spent = $state(0);

	let left = {
		get value() {
			return total - spent;
		},
		set value(v) {
			spent = total - v;
		}
	};
</script>

<label>
	<input type="range" bind:value={spent} max={total} />
	{spent}/{total} spent
</label>

<label>
	<input type="range" bind:value={left.value} max={total} />
	{left.value}/{total} left
</label>

如果您必须在 effect 中更新 $state 并且因为你读取和写入的是同一个 $state 而陷入无限循环,请使用 untrack

Svelte 中文文档

本篇已收录在掘金专栏 《Svelte 中文文档》,该系列预计 40 篇。

系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

此外我还写过 JavaScript 系列TypeScript 系列React 系列Next.js 系列冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog

通过文字建立交流本身就是一种缘分,欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant