Svelte5 抢先看!

Svelte5 抢先看!

安装

bash 复制代码
npm create svelte@latest svelte-5

当我们在执行上述命令后,首先会看到如下提示:

css 复制代码
┌  Welcome to SvelteKit!
│
◆  Which Svelte app template?
│  ● SvelteKit demo app (A demo app showcasing some of
the features of SvelteKit - play a word guessing game
that works without JavaScript!)
│  ○ Skeleton project
│  ○ Library project
└

从弹出的选项中可以看到,这里给我们提供了体验Svelte 5的选项。

接着是正常的安装依赖操作。

bash 复制代码
cd svelte-5
npm install 
npm run dev

安装完后,我们可以看到package.json里的依赖显示:

json 复制代码
{
  "svelte": "^5.0.0-next.1",
}

当然,如果不想执行上述操作也想体验Svelte 5,那可以尝试官方提供的REPL

Runes

Svelte 5最大的改动便是引入了Runes

也许大家对Runes不太熟悉,但如果说到英雄联盟里的瑞兹,相信大家耳熟能详。瑞兹的英文全称是THE RUNE MAGE ,中文翻译是符文法师。不错,Runes即符文。

为了读起来更自然点,文章中将继续以Runes来说明。Runes是一组函数式的符号,无需额外引入,可以直接使用,是Svelte5语言的特性,目前有以下Rune:

  • $state
  • $state.frozen
  • $derived
  • $derived.by
  • $effect
  • $effect.pre
  • $effect.active
  • $effect.root
  • $props
  • $inspect

$state

html 复制代码
<script>
    let count = $state(0);
</script>

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

在class中也能使用

html 复制代码
<script>
class Person {
    name = $state();

    constructor(name) {
        this.name = name;
    }
}

const person = new Person();
</script>

<input bind:value={person.name} /><br />
姓名:{person.name}

对比之前的数据声明,多了$state。但使用了$state声明的数组和对象,不再像Svelte4那样,需要使用一些小技巧来赋值更新。

在Svelte 5中,数组的更新就和在js操作中一般。

html 复制代码
<script>
const arr = $state([1,2]);

const onAdd = () => {
    arr.push(1);
}

const onSub = () => {
    arr.pop();
}
</script>

<p>
    <button on:click={onAdd}>增加</button>
    <button on:click={onSub}>减少</button>
</p>
数组:{arr.join(',')}

对象演示:

html 复制代码
<script>
const obj = $state({
    foo: {
        bar: 'hello'
    }
});

</script>

<input bind:value={obj.foo.bar} />
{obj.foo.bar}

$state.frozen

html 复制代码
<script>
let arr = $state.frozen([1,2]);

const update1 = () => {
    arr.push(1);
}

const update2 = () => {
    arr = [...arr, 1];
}
</script>

<p>
    <button on:click={update1}>更新1</button>
    <button on:click={update2}>更新2</button>
</p>
数组:{arr.join(',')}

使用了$state.frozen声明后的数组,无法直接操作数组。vscode会给出提示:

在浏览器中点击第一个更新,也会报错:

当我们调用update2时,数组能正常更新。

看下官网的一个提示:

Objects and arrays passed to $state.frozen will be shallowly frozen using Object.freeze(). If you don't want this, pass in a clone of the object or array instead.

html 复制代码
<script>
let arr = $state.frozen([{
        obj: {
                name: 'bob',
                age: 18
        }
}]);
let arr2 = $state.frozen([{name: 'carter', age: 19}]);

let arr3 = $state.frozen(['david']);

arr[0].obj.name = 'svelte';
arr2[0].name = 'svelte';
arr3[0] = 'svelte';
</script>

{JSON.stringify(arr)}
{JSON.stringify(arr2)}
{JSON.stringify(arr3)}

在上面这个例子中,arr3[0] = 'svelte';会直接报错,剩下两个赋值则不会。把报错赋值的语句去掉,我们可以看到:

$derived

$derived接收一个参数,这个参数是一个没有副作用的表达式。

html 复制代码
<script>
let count = $state(0);

let double = $derived(count * 2);

const onClick = () => {
    count++;
}
</script>

<button on:click={onClick}>更新</button><br />
count: {count}
double: {double}

我们可以传count * 2,但是不能传count++

在Svelte4中,我们要声明一个派生属性,需在$: 里进行。

$derived.by

$derived.by接收一个函数。

html 复制代码
<script>
  let arr = $state([1,2,3]);
  let total = $derived.by(() => {
    return arr.reduce((pre, cur) => pre + cur, 0);
  });

  const onAdd = () => {
    arr.push(arr.length + 1);
  }
</script>

<button on:click={onAdd}>add</button>
total: {total}

$effect

runs when the component is mounted, and again whenever count or doubled change,after the DOM has been updated.

因此,$effect相当于$: {}onMountafterUpdate的结合体。笔者对此改动表示热烈欢迎,因为本人始终觉得在有些框架中,一个组件对外提供一大串又臭又长的生命周期,着实加大了开发者的心智负担。

在Svelte 4中:

html 复制代码
<script>
  import { afterUpdate } from 'svelte';
  let width = 10;
  let height = 10;

  $: console.log('width改变', width);

  afterUpdate(() => {
    console.log('afterUpdate')
  });
</script>

width: <input type="number" bind:value={width} />

在Svelte 5中,使用$effectrune:

html 复制代码
<script>
  let width = $state(10);

  $effect(() => {
    console.log('width改变', width);
  });
</script>

width: <input type="number" bind:value={width} />

$effect.pre

用于替代beforeUpdate生命周期。

我们看个Svelte 4的例子:

html 复制代码
<script>
  import { beforeUpdate, afterUpdate } from 'svelte';
  let width = 10;

  beforeUpdate(() => {
    const dom = document.querySelector('#width');
    if (dom) {
      console.log('beforeUpdate', dom.innerHTML);
    }
  });

  afterUpdate(() => {
    const dom = document.querySelector('#width');
    if (dom) {
      console.log('afterUpdate', dom.innerHTML);
    }
  });
</script>

width: <input type="number" bind:value={width} />
<span id="width">{width}</span>

在Svelte 5中:

html 复制代码
<script>
  let width = $state(10);

  $effect.pre(() => {
    const dom = document.querySelector('#width');
    if (dom) {
      console.log('svelte5 beforeUpdate', dom.innerHTML, width);
    }
  })

  $effect(() => {
    const dom = document.querySelector('#width');
    if (dom) {
      console.log('svelte5 afterUpdate', dom.innerHTML, width);
    }
  });
</script>

width: <input type="number" bind:value={width} />
<span id="width">{width}</span>

在这里之所以要把width也一起打印出来,是因为$effect$effect.pre的后续执行需要依赖width。

$effect.active

官网给出的说明是用于判断是否在一个effect中或是否在template中。

html 复制代码
<script>
  let count = $state(0);

  $effect(() => {
    console.log('effect', count);
    console.log('isActive in $effect', $effect.active());
  });

  $effect.pre(() => {
    console.log('pre', count);
    console.log('isActive in $effect.pre', $effect.active());
  });

  let double = $derived.by(() => {
    console.log('isActive in derived.by', $effect.active());
    return count * 2;
  })

  console.log('isActive no runes', $effect.active());
</script>

<input type="number" bind:value={count} />
double: {double}
in template:{console.log('isActive in template', $effect.active())}

经过试验,在$effect$effect.pre和template中能够正常判断。我们尝试在$derived.by中使用,可是打印结果提醒我们$effect.active在其中并不适用。

$effect.root

使用#effect.root,我们可以手动地控制$effect的生效与否。

html 复制代码
<script>
let count = $state(0);

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

    return () => {
        console.log('关闭$effect');
    };
});
</script>

<button onclick={() => offEffect()}>关闭$effect</button>
<button onclick={() => count++}>更新</button>

如果不调用$effect.root的返回函数,依旧正常追踪依赖更新。当我们执行了返回函数后,可以看到$effect不再执行。

$props

很明显,用来接收props的Rune。

Svelte 4中:

html 复制代码
<script>
  export let value;
</script>

子组件:{value}

Svelte 5中:

html 复制代码
<script>
  let { value } = $props();
</script>

子组件:{value}

$inspect

当状态更新时,会打印相关信息。

html 复制代码
<script>
    let count = $state(0);
    $inspect(count); // 当count变化时console.log出来
</script>

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

Snippets

俗称片段。使用Snippets可以进行内容复用。

html 复制代码
<script>
  let arr = $state([{
    name: 'carter',
    age: 18,
    gender: '男'
  }, {
    name: 'lily',
    age: 19,
    gender: '女'
  }]);
</script>

{#snippet person({ name, age, gender })}
  <p>
    <span>姓名:{name}</span>
    <span> 年龄:{age}</span>
    <span>性别:{gender}</span>
  </p>
{/snippet}

{#each arr as item, i}
  {@render person(item)}
{/each}

使用{#snippet snippetName()}...{/snippet}来定义我们要复用的片段; 使用{@render snippetName()}来复用定义好的片段。

传递给组件

能够把片段当成一个属性传递给组件。

我们定义一个组件,可以接受header和footer片段参数:

html 复制代码
<script>
  let { header, footer } = $props();
</script>

<div>
  <header>{@render header()}</header>
  组件内部
  <footer>{@render footer()}</footer>
</div>

将我们定义好的片段传递给组件:

html 复制代码
<script>
    import Svelte5 from './Svelte5.svelte';
</script>

{#snippet header()}
    <div>头部内容</div>
{/snippet}

{#snippet footer()}
    <div>尾部内容</div>
{/snippet}

<Svelte5 {header} {footer} />

事件

事件监听

在演示Runes和Snippets时,笔者在使用到数据绑定时,仍旧使用的是Svelte4on:eventname的形式。其实在Svelte5中,关于方法的使用也有更新:从原来on:eventname的形式转变为oneventname的形式。

diff 复制代码
<script>
  const onClick = () => {
    console.log('click');
  }
</script>

- <button on:click={onClick}>click</button>
+ <button onclick={onClick}>click</button>

组件事件

使用$props()来接收方法。终于不用再使用难用的createEventDispatcher了。

html 复制代码
<script>
  let { onClick, onClick2 } = $props();
</script>

<button onclick={onClick}>click</button>
<button onclick={e => onClick2('hello svelte')}>click2</button>
html 复制代码
<script>
import Svelte5 from './Svelte5.svelte';

const onClick = (event) => {
    console.log('event', event);
}

const onClick2 = (value) => {
    console.log('value', value);
}
</script>

<Svelte5 {onClick} {onClick2} />

除了接收方法,我们还能接收插槽内容。没错,在Svelte5中,插槽的使用转而投向了jsx的写法,通过let { children } = $props()来接收插槽内容。

html 复制代码
<script>
  let { children } = $props();
</script>

<div>
  <header>头部</header>
  <main>{@render children() }</main>
  <footer>底部</footer>
</div>
html 复制代码
<script>
    import Svelte5 from './Svelte5.svelte';
</script>

<Svelte5>
    内容
</Svelte5>

这里的{@render ...}和后面介绍的Snippets有关。

事件修饰符

Svelte4的事件修饰符如下:

html 复制代码
<button on:click|once|preventDefault={handler}>...</button>

像once、preventDefault、stopPropagation等需要自己手动实现:

html 复制代码
<script>
function once(fn) {
    return function (event) {
        if (fn) fn.call(this, event);
        fn = null;
    };
}

function preventDefault(fn) {
    return function (event) {
        event.preventDefault();
        fn.call(this, event);
    };
}
</script>

<button onclick={once(preventDefault(handler))}>...</button>

capturepassivenonpassive等,Svelte5仍提供了对应的事件修饰符:

html 复制代码
<button onclickcapture={...}>...</button>

说实话,不太美观。

方法

untrack

html 复制代码
<script>
    let width = $state(10);
    let height = $state(10);
    let area;

    $effect(() => {
        console.log('width or height change', width, height);
    });
</script>

width: <input type="number" bind:value={width} />, 
height: <input type="number" bind:value={height} />

如果我们只想在width执行时输出console,那就需要不追踪height的依赖。

html 复制代码
<script>
    import { untrack } from 'svelte';

    let width = $state(10);
    let height = $state(10);

    $effect(() => {
        console.log('width or height change', width, untrack(() => height));
    });
</script>

width: <input type="number" bind:value={width} />, 
height: <input type="number" bind:value={height} />

unstate

把使用$state()生成的响应式数据(对象或数组)变成非响应式的。

演示一个Svelte 4的例子:

html 复制代码
<script>
  // svelte 4
  export let obj = {
    name: 'text'
  };

  const onUpdate = () => {
    obj.name = '' + Math.random();
  }

  $: console.log('obj change', obj.name);
</script>

<button on:click={onUpdate}>update</button><br />
obj: {obj.name}<br />

额外话题:如果我们需要$:生效,需要$: console.log(obj.name)而不是$: console.log(obj)

演示一下unstate的功能:

html 复制代码
<script>
  // svelte 5
  import { unstate } from 'svelte';

  let obj = $state({
    name: 'text'
  });
  let _obj = unstate(obj);

  const onUpdate = () => {
    obj.name = '' + Math.random();
    _obj.name = '' + Math.random();
  }

  $effect(() => {
    console.log('$state data change', obj.name);
  });

  $effect(() => {
    console.log('unstate data cahnge', _obj.name);
  })

</script>

<button onclick={onUpdate}>update</button><br />
obj: {obj.name}<br />
_obj: {_obj.name}

使用了unstate的数组在赋值后,$effect不会追踪这部分非响应式数据。

mount

Svelte 4中挂载App:

javascript 复制代码
import { mount } from 'svelte';
import App from './App.svelte';

const app = mount(App, {
    target: document.querySelector('#app'),
    props: { some: 'property' }
});

Svelte 5中挂载App:

javascript 复制代码
import App from './App.svelte'

const app = new App({
     target: document.getElementById('app'),
})

export default app

hydrate

SSR使用。

javascript 复制代码
import { hydrate } from 'svelte';
import App from './App.svelte';

const app = hydrate(App, {
    target: document.querySelector('#app'),
    props: { some: 'property' }
});

render

服务端渲染时使用。接收一个Component并返回一个带有html和head参数的对象。

javascript 复制代码
import { render } from 'svelte/server';
import App from './App.svelte';

const result = render(App, {	
    props: { some: 'property' }
});

总结

Svelte 5移除了onMountbeforeUpdateafterUpdate等生命周期,移除了createDispatcher,将组件的方法通过$props来传递,让我们不用再关注CustomEvent.detail等细节,很明显降低了学习的曲线。同时引入了Runes的概念,对数据状态能够进行更为颗粒度的控制,引入了Snippets,可以对页面内容进行Element层级的复用。

文章只介绍了较为关键的特性,更多细节内容,还请感兴趣的读者们去官网深度探索。

参考

相关推荐
Larcher2 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐14 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭27 分钟前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋2 小时前
场景模拟:基础路由配置
前端
六月的可乐2 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程