头图

Svelte 官方博客在 4.30 宣布Svelte 5 进入 RC 状态,这也意味着 Svelte 5 已经达到了一个比较稳定的状态,Svelte 5 对数据响应式的实现进行了重构,带来了更加易用的响应式 API,同时解决了 Svelte 4 实现方式带来的问题。下面我们一起来看看 Svelte 5 带来的响应式特性。

Svelte 4

先来看看在 Svelte 4 中,我们是如何在一个组件中声明响应式数据的:

<script>
  let count = 0;
  function increment() {
    count += 1;
  }

  $: content = `Total: ${count}`;
</script>

<button on:click="{increment}">Increment</button>
<div>Count: {count}</div>
<div>{content}</div>

在 Svelte 4 中我们可以在组件中通过let$来声明响应式数据/逻辑,在上面的例子中当count变化之后,content的值会进行重新计算,同时组件的 UI 也会展示最新的状态。由于 Svelte 4 是通过在编译时进行代码静态分析生成最终的响应式代码,受限于代码静态分析的局限性,在实际使用的时候会有一些边界情况需要我们特别注意:

<script>
  let count = 0;
  function increment() {
    count += 1;
  }

  function getContent() {
    return `Total: ${count}`;
  }

  $: content = getContent();
</script>

<button on:click="{increment}">Increment</button>
<div>Count: {count}</div>
<div>{content}</div>

如果按照正常的理解上面这个例子的效果应该是和之前一样的,当count更新之后content也会相应更新。可是实际上content并不会更新,之所以不会更新就是因为 Svelte 4 的编译时响应式实现,在编译时进行代码静态分析的时候,编译器并不能识别出content依赖于count,因为count并没有出现在$表达式中。所以为了让编译器知道content依赖于count,必须在$表达式中显示的表达出来。比如把count作为getContent的参数,最终改成$: content = getContent(count);

基于编译时的响应式实现带来的另一个问题是:如果我想把上面例子中关于count的逻辑从组件抽离出来以达到在多个组件之间复用这段逻辑的目的的话,我并不能简单的把代码放到一个单独的文件,然后在组件里面import这个逻辑。Svelte 4 编译时只会将.svelte中的变量声明编译成响应式的代码,所以如果我们希望在.svelte文件以外的地方声明响应式的数据需要使用 Svelte 4 中的Store API:

// counter.js
import { writable } from "svelte/store";

export function createCounter() {
  const { subscribe, update } = writable(0);

  return {
    subscribe,
    increment: () => update((n) => n + 1),
  };
}
<script>
  import { createCounter } from "./counter.js";
  const counter = createCouter();
</script>

<button on:click="{counter.increment}">Increment</button>
<div>Count: {$counter}</div>

我们需要通过 Store API 来定义响应式数据,然后在组件中通过$counter来访问,在编译之后的代码中组件内部会通过调用暴露的subscribe方法来对counter进行订阅,当数据变化之后触发视图的更新。
针对上面这些问题,Svelte 5 对底层响应式的实现进行了重构,引入了叫做Runes的 API.

Runes

Svelte 官方将新的响应式 API 叫做Runes,准确的说它们并不是 API,而是一系列原语(primitives)或者叫语法糖,最终会被编译成对应的运行时 API。

$state

Svelte 5 中,我们可以通过$state方法来声明响应式数据:

<script>
  let count = $state(0);
  let obj = $state({ value: 0 });
  function increment() {
    count += 1;
    obj.value += 1;
  }
</script>

<button on:click="{increment}">Increment</button>
<div>Count: {count}, {obj.value}</div>

上面的例子中我们声明了两个变量,一个是基本类型的 state,一个是对象类型的 state,script部分的代码将会被编译成如下代码:

import * as $ from "svelte/internal/client";
let count = $.state(0);
let obj = $.proxy({ value: 0 });

function increment() {
  $.set(count, $.get(count) + 1);
  obj.value += 1;
}

可以看到 Svelte 对于两种类型的 state 最终编译的结果是不一样的,我们可以把这里的$.state$.proxy类比到 Vue 中的refreactive两个 API,在 Vue 中如果要定义一个基本类型的状态,我们需要使用ref这个 API,在读写的时候需要通过xx.value来访问和更新状态,而在 Svelte 5 中,框架通过将对基本类型 state 的读写操作编译成$.get(xx)$.set(xx)来达到开发者可以像使用正常变量一样来使用 state 的目的。这里$.state返回的count变量其实是跟 Vue 中ref返回的变量类型一样,是一个对于基础数据类型的一个 wrapper,通过这个 wrapper 框架可以实现对基本类型数据的读写操作的拦截,从而实现依赖收集和数据监听。
对于对象类型的 state 就比较好理解了,目前大多数响应式的实现都是基于浏览器的 Proxy API 实现的依赖收集和数据监听。我们可以发现除了对基本类型数据在编译时进行了易用性的优化,Svelte 5 的响应式实现思路跟 Vue 是一样的。

$derived

$derived是 Svelte 5 中用来替代$表达式的语法。我们先来看看例子:

<script>
  let count = $state(0);
  function increment() {
    count += 1;
  }
  let content = $derived(`hello ${count}`);
</script>

<button on:click="{increment}">Increment</button>
<div>Count: {count}</div>
<div>Content: {content}</div>

$derived中可以是一个表达式或者是一个函数,函数的返回值将作为$derived的返回值。上面的$derived函数最终会被编译成:

$.derived(() => `hello ${$.get(count)}`);

很容易就可以让人联想到 Vue 中的computed方法,实际上两个函数的作用也是类似的,当count的值更新之后,content也会同时更新。
由于$derived是通过在运行时进行依赖收集的,所以就很好的解决了 Svelte 4 中由于静态代码分析带来的问题,不需要再显示告诉编译器数据依赖关系。

$effect

在 Svelte 4 中$表达式的另一个使用场景就是当依赖数据发生变更之后执行一些特定逻辑,比如:

<script>
  let count = 0;
  function increment() {
    count += 1;
  }

  $: {
    console.log(count);
  }
</script>

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

上面这个例子中当count的值更新之后,$之后的逻辑都会被执行,这个逻辑就跟 Vue 中的watchEffect方法是类似的。显然$derived并不适用于这个场景,所以 Svelte 5 中新增了$effect语法:

<script>
  let count = 0;
  function increment() {
    count += 1;
  }

  $effect(() => {
    console.log(count);
  });
</script>

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

$effect中的函数会在组件mount和count变化的时候被执行

总结

从Svelte 5 Runes中的几个API可以发现,Svelte 5通过将响应式是实现从旧版本基于编译时的依赖收集重构到新版本中基于运行时的依赖收集很好的解决了旧版本中的存在的问题;同时由于是基于运行时的响应式,那么也意味着这些Runes语法可以在任何文件中使用,不仅仅局限于.svelte文件中,也就是说开发者可以很方便的把组件中的公共逻辑抽离出来在多个组件中进行复用。

更多

除了上面提到的几个Runes以外,Svelte 5中还提供了很多其他形式的Runes,详细内容可以查看Svelte 5的最新文档
另外,本篇文章只是简单的介绍了一些Svelte 5带来的新特性,如果大家对这些特性的底层实现原理感兴趣的话,可以关注后续的文章,会有一系列关于Svelte 4和Svelte 5实现原理的相关文章进行分享。

参考


lakb248
2.8k 声望239 粉丝

Shopee(虾皮)深圳研发中心找前端/后端/移动开发,内推请投 lakb248@163.com