Svelte 5 Runes 深入解析:新一代响应式原语

Svelte 5 引入了 Runes——一种全新的响应式声明方式,彻底改变了 Svelte 的编程模型。

为什么需要 Runes?

Svelte 4 的响应式依赖于编译器的魔法:

<script>
  // Svelte 4: 赋值触发响应式
  let count = 0;
  $: doubled = count * 2;  // $: 是响应式声明
  
  // 问题:不直观的响应式边界
  function increment() {
    count += 1;  // 赋值触发更新
    // 但 array.push() 不会!
  }
</script>

$state — 响应式状态

<script>
  // Svelte 5: 明确的响应式声明
  let count = $state(0);
  let doubled = $derived(count * 2);
  
  function increment() {
    count += 1;  // 自动追踪
  }
</script>

<button onclick={increment}>
  Count: {count} (doubled: {doubled})
</button>

$derived — 派生值

<script>
  let items = $state([1, 2, 3, 4, 5]);
  
  let total = $derived(items.reduce((a, b) => a + b, 0));
  let evens = $derived(items.filter(n => n % 2 === 0));
  
  // $derived.by 用于复杂表达式
  let summary = $derived.by(() => {
    const sum = items.reduce((a, b) => a + b, 0);
    const avg = sum / items.length;
    return { sum, avg, count: items.length };
  });
</script>

$effect — 副作用

<script>
  let count = $state(0);
  
  $effect(() => {
    // 当 count 变化时自动运行
    console.log(`Count changed to ${count}`);
    
    // 可选的清理函数
    return () => {
      console.log('Cleanup before next run or unmount');
    };
  });
</script>

$props — 组件 Props

<!-- Child.svelte -->
<script>
  let { name, age = 18, onUpdate } = $props();
</script>

<p>{name} is {age} years old</p>

<!-- Parent.svelte -->
<script>
  import Child from './Child.svelte';
</script>

<Child name="Alice" age={25} />

实战:Todo 应用

<script>
  let todos = $state([]);
  let newTodo = $state('');
  
  let completedCount = $derived(
    todos.filter(t => t.done).length
  );
  
  function addTodo() {
    if (newTodo.trim()) {
      todos.push({ id: Date.now(), text: newTodo, done: false });
      newTodo = '';
    }
  }
  
  function toggleTodo(id) {
    const todo = todos.find(t => t.id === id);
    if (todo) todo.done = !todo.done;
  }
</script>

<div>
  <input bind:value={newTodo} placeholder="Add todo..." />
  <button onclick={addTodo}>Add</button>
  
  <p>{completedCount}/{todos.length} completed</p>
  
  {#each todos as todo (todo.id)}
    <div>
      <input type="checkbox" checked={todo.done} 
             onchange={() => toggleTodo(todo.id)} />
      <span class:line-through={todo.done}>{todo.text}</span>
    </div>
  {/each}
</div>

Runes vs Svelte 4 对比

特性 Svelte 4 Svelte 5 (Runes)
响应式声明 let count = 0 let count = $state(0)
派生值 $: doubled = count * 2 let doubled = $derived(count * 2)
副作用 $: { ... } $effect(() => { ... })
Props export let name let { name } = $props()
通用性 仅 .svelte 文件 .svelte 和 .svelte.js

总结

Runes 让 Svelte 的响应式更加显式和可移植。关键词:

  • $state — 响应式状态
  • $derived — 派生值
  • $effect — 副作用
  • $props — 组件属性

Runes 可以在 .svelte.js 文件中使用,打破了 Svelte 响应式与组件的耦合。