Svelte

什么是 Svelte?

Svelte 是一个现代的前端框架,由 Rich Harris 在 2016 年创建。与 React、Vue 等框架不同,Svelte 在构建时进行编译,而不是在运行时。本文章 svelet 版本为 5.39。

核心优势

1
2
3
4
5
6
7
// 传统框架(运行时)
// 需要下载框架代码到浏览器
import React from "react";
import ReactDOM from "react-dom";

// Svelte(编译时)
// 编译后生成纯JavaScript,无需框架运行时

编译时 vs 运行时

1
2
3
4
5
6
7
8
9
<!-- Svelte组件 -->
<script>
let count = 0;
function increment() {
count += 1;
}
</script>

<button onclick="{increment}">Count: {count}</button>

编译后生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 纯JavaScript,无框架代码
function create_fragment(ctx) {
let button;
return {
c() {
button = element("button");
button.textContent = `Count: ${count}`;
},
m(target, anchor) {
insert(target, button, anchor);
},
p(ctx, dirty) {
if (dirty & /*count*/ 1) {
button.textContent = `Count: ${count}`;
}
},
d(detaching) {
if (detaching) detach(button);
},
};
}

项目创建

推荐使用 SvelteKit (degit 方式淘汰)

1
npm create svelte@latest my-app

Runes

Svelte 5 引入了全新的响应式系统 Runes (符文系统),用特殊的符号($开头)来声明响应式状态,替代了传统的 let 变量和 $: 语法。

核心概念对比

Svelte 4(传统方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
let count = 0;
let doubled;

// 响应式声明
$: doubled = count * 2;

// 响应式块
$: {
console.log("Count changed:", count);
}

function increment() {
count++;
}
</script>

<button on:click="{increment}">Count: {count}, Doubled: {doubled}</button>

Svelte 5(Runes 方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
// 使用 $state 声明响应式状态
let count = $state(0);

// 使用 $derived 声明派生值
let doubled = $derived(count * 2);

// 使用 $effect 处理副作用
$effect(() => {
console.log("Count changed:", count);
});

function increment() {
count++;
}
</script>

<button onclick="{increment}">Count: {count}, Doubled: {doubled}</button>

$state

$state 用于声明响应式变量,类似于 Vue 3 的 ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
let count = $state(0);

let user = $state({
name: "Alice",
age: 25,
});

function updateUser() {
// 可以直接修改
user.name = "Bob";
user.age = 30;
}
</script>

$state.raw()

只追踪顶层属性,不追踪嵌套属性(性能更好)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script>
// 普通 $state:深度响应式
let deep = $state({
level1: {
level2: {
value: 1,
},
},
});

// $state.raw:浅响应式
let shallow = $state.raw({
level1: {
level2: {
value: 1,
},
},
});

function testDeep() {
// ✅ 深度响应式:嵌套修改会更新UI
deep.level1.level2.value++;

// ❌ 浅响应式:嵌套修改不会更新UI
shallow.level1.level2.value++; // 不触发更新

// ✅ 浅响应式:整体替换会更新UI
shallow = {
level1: {
level2: {
value: shallow.level1.level2.value + 1,
},
},
};
}
</script>

<div>
<p>Deep: {deep.level1.level2.value}</p>
<p>Shallow: {shallow.level1.level2.value}</p>
<button onclick="{testDeep}">Test</button>
</div>

$state.snapshot()

创建状态的非响应式快照(普通对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<script>
let user = $state({
name: "Alice",
age: 25,
settings: {
theme: "dark",
language: "en",
},
});

// 获取快照:返回普通对象
function saveToLocalStorage() {
// ✅ 使用快照:避免响应式开销
const snapshot = $state.snapshot(user);
localStorage.setItem("user", JSON.stringify(snapshot));

// ❌ 直接使用:会包含响应式的内部结构
// localStorage.setItem('user', JSON.stringify(user));
}

// 比较快照
let history = $state([]);

function saveHistory() {
// 保存当前状态的快照
history.push($state.snapshot(user));
}

function restoreHistory(index) {
// 从快照恢复
const snapshot = history[index];
user.name = snapshot.name;
user.age = snapshot.age;
user.settings = snapshot.settings;
}

// 传递给外部API
async function updateUserOnServer() {
const snapshot = $state.snapshot(user);
await fetch("/api/user", {
method: "POST",
body: JSON.stringify(snapshot),
});
}
</script>

<div>
<input bind:value="{user.name}" />
<button onclick="{saveHistory}">Save Snapshot</button>
<button onclick="{saveToLocalStorage}">Save to Storage</button>
</div>

$state.frozen()

创建冻结的响应式状态,对象内容不可修改(类似于 Object.freeze

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script>
// 普通 $state:可以修改
let config = $state({
apiUrl: "https://api.example.com",
timeout: 5000,
});

// $state.frozen:不可修改
let frozenConfig = $state.frozen({
apiUrl: "https://api.example.com",
timeout: 5000,
});

function tryModify() {
// ✅ 允许
config.timeout = 10000;

// ❌ 不允许(开发模式会报错)
// frozenConfig.timeout = 10000;

// ✅ 但可以整体替换
frozenConfig = {
apiUrl: "https://new-api.example.com",
timeout: 8000,
};
}
</script>

$derived

$derived 用于创建基于其他状态计算的值 (表达式),替代了 $: 响应式声明。类似于 Vue 3 的 computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
let count = $state(0);

// 简单派生
let doubled = $derived(count * 2);

// 复杂派生
let description = $derived(
count === 0
? "Zero"
: count > 0
? `Positive: ${count}`
: `Negative: ${count}`
);
</script>

$derived.by

接收函数,用于创建不适合放在简短表达式中的复杂派生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>

$effect

$effect 用于执行副作用,替代了 $: 响应式块和生命周期函数
await 之后或在 setTimeout 内部等情况下读取的值将不会被追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
let count = $state(0);
let size = $state(10);

// 基本副作用
$effect(() => {
console.log("Count changed:", count);
});

// 带清理的副作用
$effect(() => {
const interval = setInterval(() => {
count++;
}, 1000);

setTimeout(() => {
// 当 `size` 发生变化时不会追踪
size = 20;
}, 0);

// 返回清理函数
return () => {
clearInterval(interval);
};
});
</script>

理解依赖关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<script>
let user = $state({
name: 'Alice',
age: 25,
address: {
city: 'New York',
country: 'USA'
}
});

// ===== Effect 1: 只读取对象 =====
$effect(() => {
console.log('Effect 1: user object', user);
// 只追踪 user 的引用变化
});

// ===== Effect 2: 读取顶层属性 =====
$effect(() => {
console.log('Effect 2: user.name', user.name);
// 追踪 name 属性的变化
});

// ===== Effect 3: 读取嵌套属性 =====
$effect(() => {
console.log('Effect 3: user.address.city', user.address.city);
// 追踪 city 属性的变化
});

// ===== Effect 4: 读取多个属性 =====
$effect(() => {
console.log('Effect 4:', user.name, user.age);
// 追踪 name 和 age 的变化
});
</script>

<button onclick={() => { user.name = 'Bob'; }}>
修改 name
<!-- Effect 1: ❌ 不运行 -->
<!-- Effect 2: ✅ 运行 -->
<!-- Effect 3: ❌ 不运行 -->
<!-- Effect 4: ✅ 运行 -->
</button>

<button onclick={() => { user.age = 30; }}>
修改 age
<!-- Effect 1: ❌ 不运行 -->
<!-- Effect 2: ❌ 不运行 -->
<!-- Effect 3: ❌ 不运行 -->
<!-- Effect 4: ✅ 运行 -->
</button>

<button onclick={() => { user.address.city = 'LA'; }}>
修改 city
<!-- Effect 1: ❌ 不运行 -->
<!-- Effect 2: ❌ 不运行 -->
<!-- Effect 3: ✅ 运行 -->
<!-- Effect 4: ❌ 不运行 -->
</button>

<button onclick={() => { user = { name: 'Charlie', age: 35 }; }}>
替换整个对象
<!-- Effect 1: ✅ 运行 -->
<!-- Effect 2: ✅ 运行 -->
<!-- Effect 3: ✅ 运行 -->
<!-- Effect 4: ✅ 运行 -->
</button>

想监听整个对象的变化

方案 1:$state.snapshot

$state.snapshot() 会读取对象的所有属性,所以任何属性变化都会被追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
let user = $state({
name: "Alice",
age: 25,
email: "alice@example.com",
});

$effect(() => {
const snapshot = $state.snapshot(user);
console.log("User changed:", snapshot);
});

// 任何属性变化都会触发 ✅
user.name = "Bob";
user.age = 30;
</script>
方案 2:显式读取所有属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
let user = $state({
name: "Alice",
age: 25,
email: "alice@example.com",
});

$effect(() => {
const { name, age, email } = user;
console.log("User changed:", name, age, email);
saveToLocalStorage({ name, age, email });
});

// 任何被读取的属性变化都会触发 ✅
user.name = "Bob";
</script>
方案 3:$inspect (开发调试)

$inspect 只用于开发调试,生产环境会被移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
let user = $state({
name: "Alice",
age: 25,
email: "alice@example.com",
});

$inspect(user);
// 在控制台输出:任何属性变化都会显示

// 修改任何属性都会在控制台显示 ✅
user.name = "Bob"; // 显示在控制台
user.age = 30; // 显示在控制台
user.email = "bob@..."; // 显示在控制台
</script>

$effect vs useEffect

自动依赖追踪 vs 手动依赖数组

React useEffect(手动依赖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// React - 需要手动指定依赖
import { useEffect, useState } from "react";

function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState("John");

// ❌ 忘记添加依赖会导致问题
useEffect(() => {
console.log("Count:", count);
// 如果使用了name但没在依赖数组中,会出现问题
}, [count]); // 必须手动列出所有依赖

// ✅ 正确的做法
useEffect(() => {
console.log("Count:", count, "Name:", name);
}, [count, name]); // 手动维护依赖列表
}

Svelte $effect(自动依赖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
let count = $state(0);
let name = $state("John");

// ✅ 自动追踪依赖,无需手动指定
$effect(() => {
console.log("Count:", count, "Name:", name);
// Svelte自动知道这个effect依赖count和name
}); // 没有依赖数组!

// ✅ 只使用count,自动只追踪count
$effect(() => {
console.log("Count:", count);
// 只有count变化时才执行
});
</script>
细粒度响应 vs 全量执行

React useEffect

1
2
3
4
5
6
7
8
9
// React - 任何依赖变化都会重新执行整个effect
useEffect(() => {
console.log("Effect running");
console.log("Count:", count);
console.log("Name:", name);

// 即使只有count变化,整个函数都会重新执行
// 所有console.log都会再次执行
}, [count, name]);

Svelte $effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
let count = $state(0);
let name = $state("John");

// Svelte会分析代码,只有真正使用的值变化时才执行
$effect(() => {
console.log("Effect running");
console.log("Count:", count);
console.log("Name:", name);
});

// 更细粒度的控制
$effect(() => {
if (count > 0) {
console.log("Count is positive:", count);
}
// 如果count <= 0,即使count变化也不会输出
});
</script>
执行时机

React useEffect

1
2
3
4
5
6
7
8
9
// React - 在DOM更新后异步执行
useEffect(() => {
console.log("Effect runs AFTER render");
}, [count]);

// 如果需要同步执行,要用useLayoutEffect
useLayoutEffect(() => {
console.log("Effect runs BEFORE paint");
}, [count]);

Svelte $effect

1
2
3
4
5
6
7
8
9
10
11
12
<script>
// Svelte - 在状态变化后同步执行
$effect(() => {
console.log("Effect runs synchronously");
});

// $effect 会在 DOM 更新后立即执行
// 如果需要在 DOM 更新前执行,使用 $effect.pre
$effect.pre(() => {
console.log("Before DOM update");
});
</script>
无限循环处理

React useEffect - 容易无限循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Counter() {
const [count, setCount] = useState(0);

// ❌ 无限循环!
useEffect(() => {
setCount(count + 1); // 改变count
}, [count]); // count变化触发effect,effect又改变count

// ✅ 需要条件判断
useEffect(() => {
if (count < 10) {
setCount(count + 1);
}
}, [count]);

return <div>{count}</div>;
}

Svelte $effect - 自动防止无限循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
let count = $state(0);

// ❌ 这样写也会警告,但Svelte有更好的保护机制
$effect(() => {
count++; // Svelte会检测并警告
});

// ✅ Svelte推荐的写法
$effect(() => {
if (count < 10) {
count++;
}
});

// ✅ 或使用untrack避免追踪
$effect(() => {
$effect.untrack(() => {
count++; // 修改count,但不会追踪count的变化
});
});
// 注意:这个effect会执行一次(初始化时)
// 但之后count变化不会再次触发这个effect
</script>
清理函数

React useEffect

1
2
3
4
5
6
7
8
9
10
useEffect(() => {
const timer = setInterval(() => {
console.log("Tick");
}, 1000);

// 返回清理函数
return () => {
clearInterval(timer);
};
}, []); // 空数组表示只执行一次

Svelte $effect

1
2
3
4
5
6
7
8
9
10
11
12
<script>
$effect(() => {
const timer = setInterval(() => {
console.log("Tick");
}, 1000);

// 返回清理函数(相同)
return () => {
clearInterval(timer);
};
}); // 无需依赖数组
</script>

$effect 的高级用法

$effect.pre
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
let count = $state(0);

// 在DOM更新前执行
$effect.pre(() => {
console.log("Before DOM update:", count);
});

// 普通$effect在DOM更新后执行
$effect(() => {
console.log("After DOM update:", count);
});
</script>
$effect.untrack

$effect 内部使用,用于读取状态但不追踪它作为依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
let userId = $state(1);
let filter = $state("all");
let sortBy = $state("name");

$effect(() => {
// 只追踪userId
console.log("Fetching data for user:", userId);

// 使用untrack读取其他值,但不追踪它们的变化
const currentFilter = $effect.untrack(() => filter);
const currentSort = $effect.untrack(() => sortBy);

fetchData(userId, currentFilter, currentSort);
}); // 只有userId变化才会触发
</script>

使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
let userId = $state(1);
let debugMode = $state(false);

$effect(() => {
// 追踪 userId,当它变化时获取用户数据
console.log("Fetching user:", userId);
fetchUser(userId);

// 读取 debugMode 但不追踪它
$effect.untrack(() => {
if (debugMode) {
console.log("Debug: User ID is", userId);
}
});
});

// userId 变化 → 重新获取数据 ✅
// debugMode 变化 → 不会重新获取数据 ✅
</script>

$effect.untrack 到底会不会执行

1
2
3
4
5
6
7
8
9
<script>
let count = $state(0);

$effect(() => {
$effect.untrack(() => {
count++; // 这会执行吗?
});
});
</script>

会执行,但只执行一次

  1. 初始化时执行一次
    1
    count = 0 → effect运行 → untrack块执行 → count变成1
  2. 之后 count 变化不会再触发
    1
    count = 1 → effect不再运行(因为没有追踪count)
$effect.tracking

检测当前代码是否在响应式追踪上下文中,返回 boolean 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script>
let count = $state(0);

// 通用函数:可以在 effect 内外使用
function processValue(value) {
if ($effect.tracking()) {
// 在响应式上下文中
console.log("Reactive mode:", value);
return value * 2;
} else {
// 不在响应式上下文中
console.log("Non-reactive mode:", value);
return value;
}
}

// 在 effect 中调用
$effect(() => {
const result = processValue(count);
// 输出:Reactive mode: 0
// count 变化会触发重新运行
});

// 直接调用
const result = processValue(count);
// 输出:Non-reactive mode: 0
// 不会建立响应式连接
</script>

使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
import { writable } from "svelte/store";

let state = $state({ value: 0 });

// 通用函数:根据上下文返回不同格式
function getData() {
if ($effect.tracking()) {
// 在 effect 中,返回响应式数据
return state;
} else {
// 不在 effect 中,返回快照
return $state.snapshot(state);
}
}

// 在 effect 中使用
$effect(() => {
const data = getData(); // 响应式
console.log("Data:", data.value);
});

// 直接使用
const snapshot = getData(); // 非响应式快照
console.log("Snapshot:", snapshot.value);
</script>

注意事项:

  • $effect.tracking() 在组件初始化期间返回 false
  • 主要用于编写可在 effect 内外使用的通用函数
  • 适用于条件性响应式逻辑
$effect.root

手动管理 effect 生命周期,使用场景很少。主动执行清理函数,否则 effect 一直存在,使用不当会导致内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
let count = $state(0);

// 创建一个独立的effect作用域
const cleanup = $effect.root(() => {
$effect(() => {
console.log("Count:", count);
});

// 返回清理函数
return () => {
console.log("Cleanup root");
};
});

// 手动清理
function destroy() {
cleanup();
}
</script>

$props

$props 用于声明组件的 props,提供更好的类型支持。

1
2
3
4
5
6
7
8
9
<script>
let { count = 0, step = 1 } = $props();

function increment() {
count += step;
}
</script>

<button onclick="{increment}">Count: {count}</button>

$bindable

$bindable 用于创建可以从父组件双向绑定的 props,使用 bind 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Input.svelte -->
<script>
let { value = $bindable("") } = $props();
</script>

<input bind:value="{value}" />

<!-- Parent.svelte -->
<script>
let text = $state("");
</script>

<input bind:value="{text}" />
<p>You typed: {text}</p>

$inspect

$inspect 大致等同于 console.log,不同之处在于当其参数发生变化时它会重新运行。仅在开发环境有效,生产环境会被移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
let count = $state(0);
let user = $state({ name: "Alice", age: 25 });

// 自动打印count的变化
$inspect(count);

// 打印多个值
$inspect(count, user);

// 带标签
$inspect("User data:", user);
</script>

$inspect(…).with(fn)

使用自定义函数来处理调试输出,而不是默认的 console.log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
let user = $state({ name: "Alice", age: 25 });

// 默认用法(使用 console.log)
$inspect(user);

// ✅ 使用自定义函数
$inspect(user).with((type, value) => {
console.group("🔍 User Changed");
console.log("Type:", type); // "init" 或 "update"
console.log("Value:", value);
console.log("Timestamp:", new Date().toLocaleTimeString());
console.groupEnd();
});
</script>

参数说明:

  • type: "init" (初始化) 或 "update" (更新)
  • value: 当前的值

$inspect.trace(…)

不仅打印值的变化,还会显示调用栈,帮助你追踪是哪里修改了这个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
let count = $state(0);

// 普通 $inspect:只显示值
$inspect(count);

// $inspect.trace:显示值 + 调用栈
$inspect.trace(count);

function increment() {
count++; // 调用栈会显示这个函数
}

function handleClick() {
increment(); // 调用栈会显示完整路径
}

function deepClick() {
handleClick();
}
</script>

$host

$host() 用于获取自定义元素(Web Component)的宿主 DOM 元素引用,仅在配置了 customElement 选项的组件中可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Counter.svelte -->
<svelte:options customElement="my-counter" />

<script>
// 获取宿主元素的引用
const host = $host();

let count = $state(0);

$effect(() => {
// 操作宿主元素
host.style.border = count > 10 ? '2px solid red' : '1px solid gray';
host.classList.toggle('high', count > 10);
});
</script>

<button onclick={() => count++}>
Count: {count}
</button>
1
2
<!-- 使用自定义元素 -->
<my-counter></my-counter>

基础语法与组件开发

基础标记

注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- this is a comment! -->
<h1>Hello world</h1>

<!--以 svelte-ignore 开头的注释会禁用下一个标记块的警告。通常,这些是可访问性警告;确保你有充分的理由禁用它们-->
<!-- svelte-ignore a11y-autofocus -->
<input bind:value={name} autofocus />

<!--@component 开头的特殊注释,当在其他文件中悬停在组件名称上时会显示该注释-->
<!--
@component
- You can use markdown here.
- You can also use code blocks here.
- Usage:
```html
<Main name="Arethra">
\````

-->

<script>
let { name } = $props();
</script>

<main>
<h1>
Hello, {name}
</h1>
</main>

#if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script>
let user = { name: 'John', age: 25 };
let items = ['apple', 'banana', 'orange'];
let showDetails = false;
</script>

<!-- if语句 -->
{#if user.age >= 18}
<p>Welcome, {user.name}! You are an adult.</p>
{:else if user.age >= 12}
<p>Welcome, {user.name}! You are a man.</p>
{:else}
<p>Sorry, you must be 12 or older.</p>
{/if}

<!-- 条件切换 -->
<button onclick={() => showDetails = !showDetails}>
{showDetails ? 'Hide' : 'Show'} details
</button>

{#if showDetails}
<div class="details">
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
</div>
{/if}

#each

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<script>
let todos = [
{ id: 1, text: "Learn Svelte", done: false },
{ id: 2, text: "Build an app", done: true },
{ id: 3, text: "Deploy to production", done: false },
];

function addTodo() {
const text = prompt("Enter todo:");
if (text) {
todos = [
...todos,
{
id: Date.now(),
text,
done: false,
},
];
}
}

function toggleTodo(id) {
todos = todos.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
);
}
</script>

<button on:click="{addTodo}">Add Todo</button>

<ul>
{#each todos as todo, index (todo.id)}
<li class:done="{todo.done}">
<input type="checkbox" checked={todo.done} on:change={() =>
toggleTodo(todo.id)} > {todo.text}
</li>
{:else}
<p>列表为空</p>
{/each}
</ul>

#key

表达式变化重新渲染,可添加过渡效果

1
2
3
{#key value}
<div transition:fade><Component /></div>
{/key}

#await

允许根据 Promise 的三种可能状态 — 等待中(pending)、已完成(fulfilled)或已拒绝(rejected) — 进行分支处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{#await promise}
<!-- promise 正在等待中 -->
<p>等待 promise 解决中...</p>
{:then value}
<!-- promise 已完成或不是一个 Promise -->
<p>值是 {value}</p>
{:catch error}
<!-- promise 被拒绝 -->
<p>出现错误: {error.message}</p>
{/await} {#await promise then value}
<p>值是 {value}</p>
{/await} {#await promise catch error}
<p>错误是 {error}</p>
{/await} {#await import('./Component.svelte') then { default: Component }}
<Component />
{/await}

#snippet

#snippet 用于定义可复用的模板片段,类似于”模板函数”,可以接收参数并在组件内多次渲染。

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
let items = $state(['Apple', 'Banana', 'Orange']);
</script>

<!-- 定义 snippet -->
{#snippet itemCard(item, index)}
<div class="card">
<h3>#{index + 1}</h3>
<p>{item}</p>
</div>
{/snippet}

<!-- 使用 snippet -->
<div class="grid">
{#each items as item, i}
{@render itemCard(item, i)}
{/each}
</div>

语法

定义:

1
2
3
{#snippet 名称(参数1, 参数2, ...)}
<!-- 模板内容 -->
{/snippet}

渲染:

1
{@render 名称(参数1, 参数2, ...)}

应用场景

  1. 消除重复模板

  2. 作为 Props 传递 (Render Props)

1
2
3
4
5
6
7
8
9
10
<!-- List.svelte -->
<script>
let { items, renderItem } = $props();
</script>

<ul>
{#each items as item}
<li>{@render renderItem(item)}</li>
{/each}
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- App.svelte -->
<script>
import List from './List.svelte';

let users = $state([
{ id: 1, name: 'Alice', role: 'Admin' },
{ id: 2, name: 'Bob', role: 'User' }
]);
</script>

{#snippet userItem(user)}
<strong>{user.name}</strong>
<span class="role">{user.role}</span>
{/snippet}

<List items={users} renderItem={userItem} />
  1. 递归渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
let tree = $state({
name: 'Root',
children: [
{
name: 'Child 1',
children: [
{ name: 'Grandchild 1', children: [] }
]
}
]
});
</script>

<!-- 递归 snippet -->
{#snippet treeNode(node)}
<li>
<span>{node.name}</span>
{#if node.children.length > 0}
<ul>
{#each node.children as child}
{@render treeNode(child)}
{/each}
</ul>
{/if}
</li>
{/snippet}

<ul class="tree">
{@render treeNode(tree)}
</ul>
  1. 条件渲染不同布局

注意事项

  1. 必须在顶层定义,不能放在条件语句中定义

  2. 可以访问外部作用域

  3. 支持导出,需要 svelte 5.5.0 或更新版本

1
2
3
4
5
<script module>
export { add };
</script>

{#snippet add(a, b)} {a} + {b} = {a + b} {/snippet}
  1. 可选的 snippet
1
2
3
4
5
6
<script>
let { customRender } = $props();
</script>

<!-- 使用可选链 -->
{@render customRender?.() ?? 'Default content'}
  1. createRawSnippet 动态创建 snippet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createRawSnippet } from "svelte";

const snippet = createRawSnippet((param1, param2) => {
return {
// 必需:返回 HTML 字符串
render: () => string,

// 可选:初始化和清理逻辑
setup: (element) => {
// 初始化
return () => {
// 清理
};
},
};
});

{@render …}

渲染一个代码片段或者表达式

{@html …}

渲染一个有效的独立 HTML

{@const …}

用于在模板中定义局部常量,避免重复计算或提高代码可读性。只能在模板中使用,只在组件渲染时计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
let user = $state({
firstName: 'Alice',
lastName: 'Smith',
age: 25
});
</script>

<!-- 定义局部常量 -->
{@const fullName = user.firstName + ' ' + user.lastName}
{@const isAdult = user.age >= 18}

<h1>Welcome, {fullName}!</h1>
<p>Status: {isAdult ? 'Adult' : 'Minor'}</p>

使用建议:

  • ✅ 复杂计算提取为 {@const}
  • ✅ 需要在多处使用的临时值
  • ✅ 提高代码可读性
  • ❌ 简单值直接使用
  • ❌ 需要响应式用 $derived

{@debug …}

在特定变量发生变化时记录这些变量的值,并且如果打开了开发者工具,它会暂停代码执行。接受一个以逗号分隔的变量名列表(不接受任意表达式)

1
2
3
4
5
6
7
8
9
10
11
<script>
let user = {
firstname: "Ada",
lastname: "Lovelace",
};
</script>

{@debug user} {@debug user1, user2, user3}

<!-- 无法编译 -->
{@debug user.firstname}

use

Actions(动作)是在元素挂载时调用的函数。它们通过 use: 指令添加,通常会使用 $effect 以便在元素卸载时重置任何状态
可接收 3 个可选参数,节点类型,参数,由 action 创建的任何自定义事件处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
function myaction(node, data, { onswiperight: (e: CustomEvent) => void; onswipeleft: (e: CustomEvent) => void; }) {
// 节点已被挂载到 DOM 中

$effect(() => {
// 这里进行设置

node.dispatchEvent(new CustomEvent('swipeleft'));
node.dispatchEvent(new CustomEvent('swiperight'));

return () => {
// 这里进行清理
};
});
}
</script>

<div use:myaction="{data}" onswipeleft="{next}" onswiperight="{prev}">...</div>

style / class

分为 指令 和 属性 两种模式,指令优先级高于属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div style:color="red">...</div>

<div style="color: red;">...</div>

<!-- important -->
<div style:color|important="red">...</div>

<div style="color: blue;" style:color="red">这里将显示为红色</div>

<!-- 如果 `cool` 为真则结果为 `class="cool"`,否则为 `class="lame"` -->
<div class={{ cool, lame: !cool }}>...</div>
<div class:cool={cool} class:lame={!cool}>...</div>

<!-- 如果 `faded` 和 `large` 都为真,结果为 `class="saturate-0 opacity-50 scale-200"` -->
<div class={[faded && 'saturate-0 opacity-50', large && 'scale-200']}>...</div>

状态管理与数据流

组件间通信

Props(父传子)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- Parent.svelte -->
<script>
import Child from "./Child.svelte";

let message = "Hello from parent";
let count = 0;
</script>

<Child {message} {count} />

<!-- Child.svelte -->
<script>
const { message, count } = $props();
</script>

<div>
<p>Message: {message}</p>
<p>Count: {count}</p>
</div>

事件(子传父)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- Parent.svelte -->
<script>
import Child from "./Child.svelte";

let total = 0;

function handleIncrement(event) {
total += event.detail;
}
</script>

<Child on:increment="{handleIncrement}" />
<p>Total: {total}</p>

<!-- Child.svelte -->
<script>
import { createEventDispatcher } from "svelte";

const dispatch = createEventDispatcher();

function increment() {
dispatch("increment", { amount: 1 });
}
</script>

<button on:click="{increment}">Increment</button>

状态管理(Stores)

Writable Store

writable(value, start?, options?)

  • value: 初始值
  • start: 启动函数,当第一个订阅者订阅时调用,返回一个清理函数。两个参数 (set, opdate) => {},set(val): 更新 store 的值,update(fn): 通过函数更新 store 的值
  • options: 配置项,包含一个属性 equals 函数,返回值控制是否通知订阅者,默认用 === 逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const count = writable(0);

count.set(5);
count.update((n) => n + 1);

const unsubscribe = count.subscribe((value) => {
console.log("Count:", value);
});

// WebSocket 连接示例
const websocket = writable(null, (set) => {
console.log("创建 WebSocket 连接");

const ws = new WebSocket("wss://example.com");

ws.onopen = () => {
console.log("连接已建立");
set(ws);
};

ws.onmessage = (event) => {
console.log("收到消息:", event.data);
};

ws.onerror = (error) => {
console.error("WebSocket 错误:", error);
};

// 清理函数
return () => {
console.log("关闭 WebSocket 连接");
ws.close();
};
});

// 第一个订阅 → 触发 start 函数
const unsubscribe1 = websocket.subscribe((ws) => {
console.log("订阅者1:", ws);
});

// 第二个订阅 → 不会再次触发 start
const unsubscribe2 = websocket.subscribe((ws) => {
console.log("订阅者2:", ws);
});

// 取消所有订阅 → 触发清理函数
unsubscribe1();
unsubscribe2();

// 对象 store,只在真正变化时通知
const user = writable(
{ name: "Alice", age: 25 },
undefined, // 不需要 start 函数,传 undefined
{
equals: (a, b) => {
// 深度比较
return JSON.stringify(a) === JSON.stringify(b);
},
}
);

user.subscribe((value) => {
console.log("User changed:", value);
});

// 这不会触发订阅者(因为值实际上相同)
user.set({ name: "Alice", age: 25 });

// 这会触发订阅者
user.set({ name: "Bob", age: 30 });

Readable Store

1
2
3
4
5
6
7
8
9
10
11
const clock = readable(new Date(), (set) => {
// 第一个订阅者订阅时执行
const interval = setInterval(() => {
set(new Date()); // 只能在这里更新值
}, 1000);

// 返回清理函数
return () => {
clearInterval(interval);
};
});

Derived Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// stores.js
import { writable, derived } from "svelte/store";

export const todos = writable([]);
export const filter = writable("all");

export const filteredTodos = derived([todos, filter], ([$todos, $filter]) => {
switch ($filter) {
case "active":
return $todos.filter((todo) => !todo.done);
case "completed":
return $todos.filter((todo) => todo.done);
default:
return $todos;
}
});

高级特性与技巧

Module 级 Script 深度解析

什么是 Module 级 Script?

Svelte 组件中可以有两种<script>标签:

  • 普通 script - 每个组件实例都会执行
  • module script - 所有组件实例共享,只执行一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Component.svelte -->
<script context="module">
// Module级别:所有实例共享
let instanceCount = 0;

export function getInstanceCount() {
return instanceCount;
}
</script>

<script>
// 实例级别:每个实例独立
import { onMount } from "svelte";

let id = ++instanceCount;

onMount(() => {
console.log(`Instance ${id} mounted`);
});
</script>

<div>Instance #{id}</div>

核心特性

特性 Module Script 普通 Script
执行时机 模块加载时执行一次 每个实例创建时执行
作用域 所有实例共享 每个实例独立
导出 可以导出函数/变量 不能导出
生命周期 无生命周期钩子 可使用生命周期钩子
响应式 不支持$:语法 支持响应式

最佳实践

  1. 用于共享逻辑 - 工具函数、常量、配置
  2. 避免频繁更新 - module 级状态变化会影响所有实例
  3. 单例模式 - 确保全局唯一性
  4. 预加载数据 - SvelteKit 路由级数据加载
  5. 导出 API - 为组件提供外部可调用的接口
  6. 实例管理 - 追踪和协调多个实例

注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script context="module">
// ❌ 不能使用响应式语法
// $: doubled = count * 2; // 错误!

// ❌ 不能使用生命周期
// onMount(() => {}); // 错误!

// ❌ 不能访问实例级变量
// console.log(props); // 错误!

// ✅ 可以导出
export function utils() {}

// ✅ 可以定义共享状态
let shared = 0;

// ✅ 可以导入其他模块
import { something } from "somewhere";
</script>

决策树:何时使用哪种方案

1
2
3
4
5
6
7
8
9
10
11
需要在组件间共享逻辑?
├─ 是:逻辑是否与特定组件紧密相关?
│ ├─ 是:需要访问组件实例吗?
│ │ ├─ 是:使用 Module Script ✅
│ │ └─ 否:是否是纯工具函数?
│ │ ├─ 是:使用独立JS文件 ✅
│ │ └─ 否:使用 Module Script ✅
│ └─ 否:跨多个不同组件复用吗?
│ ├─ 是:使用独立JS文件 ✅
│ └─ 否:使用 Module Script ✅
└─ 否:使用普通 script 标签 ✅

实际建议

使用独立 JS 文件的场景:

  1. 纯工具函数 - 完全无状态的辅助函数

    1
    2
    3
    // utils/format.js
    export function formatDate(date) {...}
    export function formatCurrency(amount) {...}
  2. 跨项目复用 - 多个项目共享的逻辑

    1
    2
    // @mycompany/utils
    export function validate(data) {...}
  3. 第三方集成 - API 客户端、SDK 封装

    1
    2
    // services/api.js
    export class ApiClient {...}
  4. 配置文件 - 纯数据配置

    1
    2
    3
    // config/constants.js
    export const API_BASE_URL = '...';
    export const ROUTES = {...};

使用 Module Script 的场景:

  1. 组件级单例 - Modal、Tooltip 等需要协调的组件
  2. 实例管理 - 需要追踪所有组件实例
  3. 组件特定缓存 - 只在该组件使用的缓存策略
  4. SvelteKit 预加载 - 路由级数据加载(必须)
  5. 组件 API 导出 - 暴露组件的控制接口
  6. 性能优化 - 需要在组件间共享的昂贵计算结果

组件插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- Card.svelte -->
<div class="card">
<header>
<slot name="header">Default header</slot>
</header>

<main>
<slot>Default content</slot>
</main>

<footer>
<slot name="footer">Default footer</slot>
</footer>
</div>

<!-- 使用插槽 -->
<Card>
<h2 slot="header">Custom Header</h2>
<p>This is the main content</p>
<button slot="footer">Action</button>
</Card>

上下文 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- Parent.svelte -->
<script>
import { setContext } from "svelte";
import Child from "./Child.svelte";

const theme = {
primary: "#ff3e00",
secondary: "#676778",
};

setContext("theme", theme);
</script>

<Child />

<!-- Child.svelte -->
<script>
import { getContext } from "svelte";
import GrandChild from "./GrandChild.svelte";

const theme = getContext("theme");
</script>

<div style="color: {theme.primary}">
<p>Child component</p>
<GrandChild />
</div>

<!-- GrandChild.svelte -->
<script>
import { getContext } from "svelte";

const theme = getContext("theme");
</script>

<div style="background: {theme.secondary}">
<p>GrandChild component</p>
</div>

动作(Actions)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script>
function longpress(node, duration) {
let timer;

function handleMouseDown() {
timer = setTimeout(() => {
node.dispatchEvent(new CustomEvent("longpress"));
}, duration);
}

function handleMouseUp() {
clearTimeout(timer);
}

node.addEventListener("mousedown", handleMouseDown);
node.addEventListener("mouseup", handleMouseUp);
node.addEventListener("mouseleave", handleMouseUp);

return {
destroy() {
node.removeEventListener("mousedown", handleMouseDown);
node.removeEventListener("mouseup", handleMouseUp);
node.removeEventListener("mouseleave", handleMouseUp);
},
};
}

function handleLongPress() {
alert("Long press detected!");
}
</script>

<button use:longpress="{500}" on:longpress="{handleLongPress}">
Press and hold me
</button>

过渡动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script>
import { fade, fly, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';

let visible = true;
let items = ['Apple', 'Banana', 'Orange'];
</script>

<button on:click={() => visible = !visible}>
Toggle
</button>

{#if visible}
<div
transition:fade={{ duration: 300 }}
class="box"
>
Fade transition
</div>
{/if}

{#each items as item, i (item)}
<div
transition:fly={{
y: 200,
duration: 300,
delay: i * 100,
easing: quintOut
}}
>
{item}
</div>
{/each}

<style>
.box {
width: 100px;
height: 100px;
background: #ff3e00;
margin: 10px;
}
</style>

性能优化与最佳实践

组件优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 使用keyed each -->
{#each items as item (item.id)}
<ItemComponent {item} />
{/each}

<!-- 避免不必要的重新渲染 -->
<script>
let expensiveValue;

$: expensiveValue = expensiveCalculation(data);

function expensiveCalculation(data) {
// 复杂计算
return data.reduce((acc, item) => acc + item.value, 0);
}
</script>

内存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import { onDestroy } from "svelte";

let interval;
let count = 0;

onMount(() => {
interval = setInterval(() => {
count++;
}, 1000);
});

onDestroy(() => {
if (interval) {
clearInterval(interval);
}
});
</script>

代码分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
import { onMount } from "svelte";

let HeavyComponent;

onMount(async () => {
const module = await import("./HeavyComponent.svelte");
HeavyComponent = module.default;
});
</script>

{#if HeavyComponent}
<svelte:component this="{HeavyComponent}" />
{/if}

虚拟 DOM 深度解析:Svelte vs Vue vs React

为什么 Svelte 不需要虚拟 DOM

编译时 vs 运行时

传统框架(React/Vue)的虚拟 DOM 流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
// React示例
function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</div>
);
}

// 运行时流程:
// 1. 状态变化 → 2. 重新渲染 → 3. 生成虚拟DOM → 4. Diff算法 → 5. 更新真实DOM

Svelte 的编译时优化:

1
2
3
4
5
6
7
8
<!-- Svelte组件 -->
<script>
let count = 0;
</script>

<button on:click={() => count++}>
Count: {count}
</button>

编译后生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Svelte编译后的代码
function create_fragment(ctx) {
let button;
let t0;
let t1;
let mounted;
let dispose;

return {
c() {
button = element("button");
t0 = text("Count: ");
t1 = text(/*count*/ ctx[0]);
},
m(target, anchor) {
insert(target, button, anchor);
append(button, t0);
append(button, t1);
if (!mounted) {
dispose = listen(button, "click", /*click_handler*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) {
set_data(t1, /*count*/ ctx[0]);
}
},
d(detaching) {
if (detaching) detach(button);
mounted = false;
dispose();
},
};
}

// 状态变化时直接更新DOM
function click_handler(event) {
count = count + 1;
// 直接更新对应的DOM节点,无需虚拟DOM
}

核心原理对比

特性 React/Vue Svelte
处理时机 运行时 编译时
DOM 更新 虚拟 DOM + Diff 直接 DOM 操作
性能开销 框架运行时 + 虚拟 DOM 纯 JavaScript
包大小 包含框架代码 仅业务代码
更新策略 批量更新 精确更新

Svelte 的精确更新机制

编译时分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 原始Svelte组件 -->
<script>
let name = "World";
let count = 0;
let items = ["a", "b", "c"];
</script>

<h1>Hello {name}!</h1>
<p>Count: {count}</p>
<ul>
{#each items as item}
<li>{item}</li>
{/each}
</ul>

编译时分析结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 编译器分析出:
// - name变化只影响h1元素
// - count变化只影响p元素
// - items变化影响整个ul元素

// 生成精确的更新函数
function update_name(ctx, dirty) {
if (dirty & /*name*/ 1) {
set_data(t1, /*name*/ ctx[0]);
}
}

function update_count(ctx, dirty) {
if (dirty & /*count*/ 2) {
set_data(t3, /*count*/ ctx[1]);
}
}

响应式更新机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Svelte的响应式系统
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= 1 << i % 31;
}

// 精确更新
function update(component) {
const dirty = component.$$.dirty;
component.$$.dirty = [-1];

// 只更新标记为dirty的部分
if (dirty[0] & /*name*/ 1) {
update_name(component, dirty);
}
if (dirty[0] & /*count*/ 2) {
update_count(component, dirty);
}
}

性能对比分析

内存使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// React应用内存使用
const ReactApp = () => {
const [data, setData] = useState(largeDataSet);

// 每次更新都会:
// 1. 创建新的虚拟DOM树
// 2. 执行Diff算法
// 3. 生成更新指令
return <DataList data={data} />;
};

// Svelte应用内存使用
<script>
let data = largeDataSet; // 编译时已经知道: // 1. 哪些部分需要更新 // 2.
如何直接更新DOM // 3. 无需额外的虚拟DOM开销
</script>;

更新性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 性能测试对比
const performanceTest = {
// React: 1000个组件更新
react: () => {
// 1. 创建1000个虚拟DOM节点
// 2. 执行Diff算法
// 3. 批量更新DOM
// 总时间: ~5ms
},

// Vue: 1000个组件更新
vue: () => {
// 1. 响应式系统触发
// 2. 创建虚拟DOM
// 3. Diff + 更新
// 总时间: ~3ms
},

// Svelte: 1000个组件更新
svelte: () => {
// 1. 直接更新对应的DOM节点
// 总时间: ~1ms
},
};

总结

  1. 编译时优化
  • 在构建时就分析出所有可能的更新路径
  • 生成精确的更新代码,无需运行时计算
  1. 直接 DOM 操作
  • 跳过虚拟 DOM 的创建和 Diff 过程
  • 直接操作真实 DOM,减少中间层开销
  1. 精确更新
  • 只更新真正变化的部分
  • 避免不必要的 DOM 操作
  1. 零运行时
  • 不需要框架代码在浏览器中运行
  • 生成的代码就是纯 JavaScript
  1. 更好的性能
  • 更小的包体积
  • 更快的更新速度
  • 更低的内存使用

Svelte 编译原理

编译器架构

Svelte 编译器的工作流程分为四个主要阶段:

1
源代码 → 解析(Parse) → 分析(Analyze) → 转换(Transform) → 生成(Generate) → 输出代码

Parse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// Svelte编译器解析过程
import { parse } from 'svelte/compiler';

const source = `
<script>
let count = 0;
</script>

<button on:click={() => count++}>
Count: {count}
</button>
`;

// 解析生成AST(抽象语法树)
const ast = parse(source);

console.log(ast);
// 输出结构:
{
html: {
type: 'Fragment',
children: [
{
type: 'Element',
name: 'button',
attributes: [
{
type: 'EventHandler',
name: 'click',
expression: { /* ... */ }
}
],
children: [
{ type: 'Text', data: 'Count: ' },
{ type: 'MustacheTag', expression: { /* count */ } }
]
}
]
},
instance: {
type: 'Script',
content: { /* let count = 0; */ }
}
}

Analyze

编译器会进行静态分析,建立依赖图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 依赖分析示例
class DependencyAnalyzer {
constructor(ast) {
this.variables = new Map();
this.dependencies = new Map();
}

analyze() {
// 1. 识别所有变量声明
this.findVariables();

// 2. 建立依赖关系
this.buildDependencyGraph();

// 3. 标记哪些变量变化会影响DOM
this.markDOMDependencies();
}

findVariables() {
// 分析: let count = 0;
this.variables.set("count", {
kind: "let",
mutated: true, // 检测到 count++
referenced_in_template: true, // 在模板中使用
});
}

buildDependencyGraph() {
// 分析响应式声明
// $: doubled = count * 2;
this.dependencies.set("doubled", ["count"]);
}

markDOMDependencies() {
// count变化 → 更新button的文本节点
return {
count: [{ type: "text", node: "button_text" }],
};
}
}

Transform

将 AST 转换为可执行的 JavaScript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 转换器工作原理
class ComponentTransformer {
transform(ast, dependencies) {
return {
create: this.generateCreateFunction(ast),
update: this.generateUpdateFunction(dependencies),
destroy: this.generateDestroyFunction(ast),
};
}

generateCreateFunction(ast) {
// 生成创建DOM的代码
return `
function create_fragment(ctx) {
let button;
let t0, t1;

return {
c() {
button = element("button");
t0 = text("Count: ");
t1 = text(ctx[0]); // ctx[0] = count
},
m(target, anchor) {
insert(target, button, anchor);
append(button, t0);
append(button, t1);
},
p(ctx, [dirty]) {
if (dirty & 1) { // 检查count是否改变
set_data(t1, ctx[0]);
}
},
d(detaching) {
if (detaching) detach(button);
}
};
}
`;
}
}

Generate

最终生成的 JavaScript 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 完整的编译输出
function instance($$self, $$props, $$invalidate) {
let count = 0;

// 事件处理器
const click_handler = () => {
$$invalidate(0, count++, count);
};

return [count, click_handler];
}

class Component extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}

export default Component;

响应式编译原理

位运算优化

Svelte 使用位运算来追踪哪些变量发生了变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// dirty标记系统
const DIRTY_BITS = {
count: 1 << 0, // 0001 (1)
name: 1 << 1, // 0010 (2)
age: 1 << 2, // 0100 (4)
email: 1 << 3, // 1000 (8)
};

// 标记count和age发生变化
let dirty = DIRTY_BITS.count | DIRTY_BITS.age; // 0101 (5)

// 检查count是否变化
if (dirty & DIRTY_BITS.count) {
// 更新count相关的DOM
}

// 检查age是否变化
if (dirty & DIRTY_BITS.age) {
// 更新age相关的DOM
}

响应式系统底层实现

响应式变量的本质

1
2
3
4
5
6
7
8
9
10
11
12
// 普通变量
let count = 0;
count++; // 不会触发更新

// Svelte的响应式变量(编译后)
let count = 0;
function increment() {
$$invalidate(0, (count = count + 1));
// ^ ^
// | |
// 索引 新值
}

$$invalidate 函数原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Svelte运行时的核心函数
function $$invalidate(index, value) {
// 1. 更新值
component.$$.ctx[index] = value;

// 2. 标记为dirty
make_dirty(component, index);

// 3. 调度更新
schedule_update();

return value;
}

function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
// 首次标记dirty,加入更新队列
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}

// 使用位运算标记
component.$$.dirty[(i / 31) | 0] |= 1 << i % 31;
}

更新调度机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 微任务调度
let update_scheduled = false;
const resolved_promise = Promise.resolve();
const dirty_components = [];

function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}

function flush() {
const seen_callbacks = new Set();

do {
// 从dirty_components中取出组件
while (dirty_components.length) {
const component = dirty_components.shift();
update(component);
}
} while (dirty_components.length);

update_scheduled = false;
}

function update(component) {
if (component.$$.fragment !== null) {
// 执行beforeUpdate回调
component.$$.before_update.forEach(run_callback);

const dirty = component.$$.dirty;
component.$$.dirty = [-1];

// 调用组件的p方法(patch)
component.$$.fragment.p(component.$$.ctx, dirty);

// 执行afterUpdate回调
component.$$.after_update.forEach(run_callback);
}
}

响应式语句的依赖追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 源代码
let firstName = "John";
let lastName = "Doe";
$: fullName = `${firstName} ${lastName}`;
$: greeting = `Hello, ${fullName}!`;

// 编译后的依赖追踪
$$self.$$.update = () => {
// fullName依赖firstName和lastName
if ($$self.$$.dirty & /*firstName, lastName*/ 3) {
$$invalidate(2, (fullName = `${firstName} ${lastName}`));
}

// greeting依赖fullName
if ($$self.$$.dirty & /*fullName*/ 4) {
$$invalidate(3, (greeting = `Hello, ${fullName}!`));
}
};

事件系统与 DOM 操作

事件委托优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Svelte的事件处理
function listen(node, event, handler, options) {
node.addEventListener(event, handler, options);

return () => {
node.removeEventListener(event, handler, options);
};
}

// 事件修饰符实现
function preventDefault(fn) {
return function (event) {
event.preventDefault();
return fn.call(this, event);
};
}

function stopPropagation(fn) {
return function (event) {
event.stopPropagation();
return fn.call(this, event);
};
}

// 使用
// <button on:click|preventDefault={handleClick}>
const dispose = listen(button, "click", preventDefault(handleClick));

DOM 操作优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 高效的DOM操作函数
function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}

function detach(node) {
node.parentNode.removeChild(node);
}

function element(name) {
return document.createElement(name);
}

function text(data) {
return document.createTextNode(data);
}

function set_data(text, data) {
data = "" + data;
if (text.data !== data) {
text.data = data;
}
}

// 批量操作优化
function claim_element(nodes, name, attributes) {
// 从现有DOM中"认领"节点,用于SSR水合
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.nodeName === name) {
return node;
}
}

return element(name);
}

SSR 与水合(Hydration)

服务端渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SSR渲染函数
function render(component, options = {}) {
const result = component.render(options.props);

return {
html: result.html,
css: result.css,
head: result.head,
};
}

// 组件的SSR实现
class Component {
static render(props) {
const { count } = props;

return {
html: `
<button>
Count: ${count}
</button>
`,
css: {
code: ".button { color: red; }",
map: null,
},
};
}
}

水合过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 客户端水合
function hydrate(component, options) {
const target = options.target;
const anchor = options.anchor;

// claim模式:复用服务端渲染的DOM
const nodes = Array.from(target.childNodes);

component.$$ = {
...component.$$,
fragment: component.create_fragment(component.$$.ctx),
};

// 认领现有节点
component.$$.fragment.l(nodes);

// 挂载事件监听器
component.$$.fragment.m(target, anchor);

flush();
}

// claim实现
function claim_fragment(nodes) {
let i = 0;

return {
l(nodes) {
button = claim_element(nodes, "BUTTON", {});
const button_nodes = children(button);
t0 = claim_text(button_nodes, "Count: ");
t1 = claim_text(button_nodes, ctx[0]);
button_nodes.forEach(detach);
},
};
}

性能优化原理

编译时优化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 静态提升
// 源代码
<div class="static">
<p>Static content</p>
</div>;

// 编译优化
const static_content = `
<div class="static">
<p>Static content</p>
</div>
`;

// 2. 常量折叠
$: result = 2 + 3; // 编译时计算为 5

// 3. 死代码消除
if (false) {
console.log("Never runs"); // 编译时移除
}

运行时优化技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. 使用keyed each避免重排
{#each items as item (item.id)}
<Item {item} />
{/each}

// 编译后会生成高效的更新逻辑
function update_each(changed, ctx) {
const items = ctx.items;

for (let i = 0; i < items.length; i++) {
const child_ctx = get_each_context(ctx, items, i);

if (each_blocks[i]) {
each_blocks[i].p(child_ctx, changed);
} else {
each_blocks[i] = create_each_block(child_ctx);
each_blocks[i].c();
each_blocks[i].m(target, anchor);
}
}
}

// 2. 避免不必要的响应式
let count = 0;
const MAX = 100; // 常量,不会被标记为响应式

框架对比

React vs Vue vs Svelte

核心设计理念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// React: UI = f(state)
// 函数式编程思想,不可变数据
function App() {
const [state, setState] = useState(initial);
return <View state={state} />;
}

// Vue: 响应式数据驱动
// 面向对象,可变数据 + Proxy拦截
export default {
data() {
return { state: initial };
},
template: `<View :state="state" />`
}

// Svelte: 编译时优化
// 声明式,编译时转换
<script>
let state = initial;
</script>
<View {state} />

更新机制对比

框架 检测变化 更新策略 性能特点
React setState 触发 虚拟 DOM Diff 需要手动优化(memo, useMemo)
Vue Proxy 拦截 依赖追踪 + 虚拟 DOM 自动收集依赖,精准更新
Svelte 编译时注入 直接 DOM 操作 最小化运行时,极致性能

内存管理对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// React: 虚拟DOM树占用内存
class ReactComponent {
render() {
// 每次渲染创建新的虚拟DOM对象
return {
type: 'div',
props: { children: [...] },
_owner: this,
_store: {}
};
}
}

// Vue: 响应式代理 + 虚拟DOM
const vueData = new Proxy({}, {
get(target, key) {
// 依赖收集,存储订阅关系
dep.depend();
return target[key];
}
});

// Svelte: 最小内存占用
// 编译后只保留必要的状态
let count = 0; // 仅存储原始值,无额外包装

状态管理进阶

复杂状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// stores/app.js
import { writable, derived } from "svelte/store";

// 全局应用状态
function createAppStore() {
const { subscribe, set, update } = writable({
user: null,
theme: "light",
locale: "en",
loading: false,
error: null,
});

return {
subscribe,

// 用户相关
setUser: (user) => update((s) => ({ ...s, user })),
logout: () => update((s) => ({ ...s, user: null })),

// 主题切换
toggleTheme: () =>
update((s) => ({
...s,
theme: s.theme === "light" ? "dark" : "light",
})),

// 加载状态
setLoading: (loading) => update((s) => ({ ...s, loading })),

// 错误处理
setError: (error) => update((s) => ({ ...s, error })),
clearError: () => update((s) => ({ ...s, error: null })),

// 重置
reset: () =>
set({
user: null,
theme: "light",
locale: "en",
loading: false,
error: null,
}),
};
}

export const appStore = createAppStore();

// 派生状态
export const isAuthenticated = derived(appStore, ($app) => $app.user !== null);

export const isDarkMode = derived(appStore, ($app) => $app.theme === "dark");

中间件模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// stores/middleware.js
function createMiddleware(store) {
const { subscribe, set, update } = store;

// 日志中间件
const logger = (action, state) => {
console.log("[Store]", action, state);
};

// 持久化中间件
const persist = (state) => {
localStorage.setItem("app-state", JSON.stringify(state));
};

return {
subscribe,

set: (value) => {
logger("SET", value);
persist(value);
set(value);
},

update: (fn) => {
update((state) => {
const newState = fn(state);
logger("UPDATE", newState);
persist(newState);
return newState;
});
},
};
}

// 使用
export const store = createMiddleware(writable({}));

微前端与组件库开发

Web Components 集成

1
2
3
4
5
6
7
8
9
10
11
<!-- Button.svelte -->
<svelte:options tag="my-button" />

<script>
export let variant = "primary";
export let disabled = false;
</script>

<button class="btn {variant}" {disabled}>
<slot />
</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 编译为Web Component
// vite.config.js
export default {
plugins: [
svelte({
compilerOptions: {
customElement: true,
},
}),
],
};

// 使用
// <my-button variant="primary">Click me</my-button>

组件库架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// components/index.js
export { default as Button } from './Button.svelte';
export { default as Input } from './Input.svelte';
export { default as Modal } from './Modal.svelte';
export { default as Tabs } from './Tabs.svelte';

// 主题系统
// theme.js
export const theme = {
colors: {
primary: '#ff3e00',
secondary: '#676778',
success: '#40b883',
danger: '#e74c3c'
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px'
}
};

// ThemeProvider.svelte
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';

export let theme;

const themeStore = writable(theme);
setContext('theme', themeStore);

$: themeStore.set(theme);
</script>

<slot />