svelte5 Runes

220 阅读1分钟

中文翻译为符号,这是一组强大的语法,用于控制 svelte 组件内部的反应性,并且支持在 . svelte.js 和 .svelte.ts 模块内部支持使用

如何声明

之前我们会使用 let=export 关键字和 $来监听特定变化的内容,目前我们要使用响应状态,使用$state

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

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

  <button on:click={increment}>
  clicks: {count}
</button>

为什么会改变

可能用惯 svelte4的觉得直接使用 let这种语句就已经很好了,本身就是响应式,那为什么要修改?

原因是因为随着应用程序的复杂性增加,要弄清哪些是反应的,只能通过顶层的let声明,这可能会导致混淆,尤其是在 .svlete文件中和 .js文件中的区分,让重构变得很困难,我们只能通过 store的形式进行使用

svelte 和 ts/js 扩页面使用

老版本实现方法

以前我们想维护一个状态,可以在 sveltets中 公用一个变量,我们需要使用 store通过 subscribe方法来实现例如

import { writable } from 'svelte/store';

export function createCounter() {
	const { subscribe, update } = writable(0);
	return {
		subscribe,
		increment: () => update((n) => n + 1)
	};
}
<script lang="ts">
	import type { PageData } from './$types';
	import { createCounter } from './model';

	export let data: PageData;

	const counter = createCounter();
</script>

<button on:click={counter.increment}>点击{$counter}</button>

新版本实现方法

export function createCounter() {
	let count = $state(0);

	return {
		get count() {
			return count;
		},
		increment: () => (count += 1)
	};
}

因为我们函数中 get count() 我们永远拿到的是最新值,而不是创建函数中的值

$state 创建

普通声明

//普通声明
<script>
	let count = $state(0);
</script>

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

类生命

//类声明
class Todo {
	done = $state(false);
	text = $state();

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

$state.raw

使用此方法,状态不能变异,只能被重新分配,换句话说,如果像更新对象或者数组,不能直接 push 只能完全替换数组或对象,官网说,可以提高大型数组或者对象的性能,因为它避免了使用它们具有反应性的成本

<script>
	let numbers = $state.raw([1, 2, 3]);
</script>

<button onclick={() => numbers = [...numbers, numbers.length + 1]}>
	push
</button>

<button onclick={() => numbers = numbers.slice(0, -1)}> pop </button>

<p>
	{numbers.join(' + ') || 0}
	=
	{numbers.reduce((a, b) => a + b, 0)}
</p>

$state.snapshot

是用于获取当前状态快照的一个功能,允许你获取应用的当前值,在开发过程中很有使用价值,尤其在调试状态变化时,能够提供状态的及时视图

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

	function onclick() {
		// Will log `{ count: ... }` rather than `Proxy { ... }`
		console.log($state.snapshot(counter));
	}
</script>

$state.is

主要应用语检查状态类型,确定给的定状态是否属于特定的类型。对于条件渲染和状态管理有很大帮助

<script>
	let foo = $state({});
	let bar = {};

	foo.bar = bar;

	console.log(foo.bar === bar); // false — `foo.bar` is a reactive proxy
	console.log($state.is(foo.bar, bar)); // true
</script>

案例

<script lang="ts">
	let currentState = $state('loading'); 
</script>

{#if $state.is(currentState, 'loading')}
	<p>Loading...</p>
{:else if $state.is(currentState, 'error')}
	<p>Error</p>
{:else if $state.is(currentState, 'success')}
	<p>Success</p>
{/if}

$derived

用于创建派生状态(derived state)的功能,它允许你基于一个或者多个现有的状态值计算新的状态值,并自动维护这派生值的反应性

对于上一版本中的 $ 可以有效的监控更改的属性变化

<script>
  export let width;
	export let height;

	$: area = width * height; //同时会监控width和height变化
</script>

然而真正开发我们会建立一个函数通过传参的形式进行监控,让监控对象变得很模糊,重构很麻烦,像下面如果你只传一个 width,那 height发生了变化并不会重新计算

const multiplyByHeight = (width) => width * height;
$: area = multiplyByHeight(width);  //只监控width

新增了 $derived``$effect用来监控属性变化

<script>
	let { width, height } = $props(); // instead of `export let`

	const area = $derived(width * height);

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

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

$effect

用于声明副作用,副作用是在状态或其他计算值发生变化时执行的代码,通常用于处理需要在数据变化时执行的操作,如更新DOM、发起网络请求或进行其他不直接涉及计算的新任务

监控dom

会在组件挂载时运行,并将在它读取的值发生任何更改后重新运行

<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);

		// this will re-run whenever `color` or `size` change
		context.fillStyle = color;
		context.fillRect(0, 0, size, size);
	});
</script>
//组件安装到dom以及值发生变化时运行副作用
<canvas bind:this={canvas} width="100" height="100" />

注意:异步读取值,或者setTimeout中,将不会被跟踪

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

	// this will re-run whenever `color` changes...
	context.fillStyle = color;

	setTimeout(() => {
		// ...but not when `size` changes
		context.fillRect(0, 0, size, size);
	}, 0);
});

监听对象更改

读取对象更改时重新运行,而不是在它内部的属性更改时从新运行,如果想监听属性发生变化可以使用 $inspect

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

	// this will run once, because `state` is never reassigned (only mutated)
	$effect(() => {
		state;
	});

	// this will run whenever `state.value` changes...
	$effect(() => {
		state.value;
	});

	// ...and so will this, because `derived` is a new object each time
	$effect(() => {
		derived;
	});
</script>

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

<p>{state.value} doubled is {derived.value}</p>

返回函数

可以返回一个函数,该函数将在效果重新运行前立即执行,并且在被销毁之前运行

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

	$effect(() => {
		// This will be recreated whenever `milliseconds` changes
		const interval = setInterval(() => {
			count += 1;
		}, milliseconds);

		return () => {
			// if a callback is provided, it will run
			// a) immediately before the effect re-runs
			// b) when the component is destroyed
			clearInterval(interval);
		};
	});
</script>

<h1>{count}</h1>

<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>

什么时候不能使用?

官网说,对分析和直接DOM操作有用,而不是经常使用的工具,特别是,避免使用它来同步状态

错误使用1

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

	// don't do this!
	$effect(() => {
		doubled = count * 2;
	});
</script>

正确使用1

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

错误使用2

想更新两个的状态,一个状态更新,另一个也随之更新

<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} spent
</label>

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

正确使用2

<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} spent
</label>

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

代码优化

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

untrack

用于同步更新DOM的方法,他确保在调用时,所有待处理的状态更新和DOM渲染都被立即执行,而不是等待下一个事件循环

Svelte5 中的 flushSyncVue3 中的 nextTick

相似与区别

  • 相似:两者都用于确保 DOM 更新后的操作能够在特定时立即执行,而避免因异步更新导致的状态不一致或视图不同步问题
  • 区别:nextTick Vue 中通常是异步的,等待下一个 tick后执行回调
  • flushSyncSvelte中是同步的,立即执行待处理的更新并执行传入的回调

在实际使用中,flushSync 可以帮助开发者在需要立即看到渲染效果的场景中避免不确定性,nextTick 在 DOM 更新完成后执行某些逻辑,处理与异步渲染相关的需求

$effect.pre

在极少数情况下,需要再 DOM 更新之前运行代码,老版本中 beforUpdate 和 afterUpdate 弃用

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

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

	// ...

	$effect.pre(() => {
		if (!div) return; // not yet mounted

		// reference `messages` array length so that this code re-runs whenever it changes
		messages.length;

		// autoscroll when new messages are added
		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.tracking

用于跟踪和管理相应式状态的副作用,它主要用于开发和调试,帮助开发整更好的理解和控制组件的相应式更新

<script>
	console.log('in component setup:', $effect.tracking()); // false

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

<p>in template: {$effect.tracking()}</p> <!-- true -->

$effect.root

是一个高级功能,它创建一个不会自动清理的跟踪范围,用于访问组件的根响应式上下文

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

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

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

$props

这是一个新特性,用于简化组件属性的管理,这个功能在 svelte4 中并不存在

优势:

  • 动态属性访问:允许你在组件内部以编程方式访问所有传递的组件属性,无需逐一列出属性
  • 简化属性处理:避免在组件内部手动处理大量属性

替换内容

  • 单独属性声明
<script>
export let name;
export let age
//svelte5 直接使用
let {name,age} = $props()
</script>
  • svelte4 中可以使用 $$props,(这是 Svelte内部机制,不是官方文档中正式 API,不如 $props() 函数方便直观)

$bindable()

主要用于双向数据绑定,组件的属性可以直接绑定到父组件的状态

script>
let {value:$bindable()} = $props();
</script>

<input bind:value={value} />

$inspect

是一个新调试工具,方便调试组件的状态和属性

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

	$inspect(count, message); // will console.log when `count` or `message` change
</script>

<button onclick={() => count++}>Increment</button>
<input bind:value={message} />

使用回调函数调用这个属性,回调函数的第一个参数是 initupdate

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

	$inspect(count).with((type, count) => {
		if (type === 'update') {
			debugger; // or `console.trace`, or whatever you want
		}
	});
</script>

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

还提供了便利的方法来追踪和调试状态变化的源头,这种方法可以帮助你更容易找到导致状态变化的具体代码位置和调用路径

$inspect(stuff).with(console.trace);