Svelte 官方入门教程(8) - 状态管理

svelte cover

上一讲的是生命周期,今天咱们继续开始状态管理

一、可写状态

并非所有的状态都属于在组件层次的结构内。某些时候,有些状态需要被多个毫不相干的组件或普通的 JavaScript 模块访问。

Svelte 中,我们使用 stores 来执行此操作。 store 只是一个带有 subscribe 方法的对象,当 store 值发生变化时,它允许相关方得到通知。 在 App.svelte 中,count 是一个 store,我们在 count.subscribe 回调中设置 countValue
App.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';

let countValue;

count.subscribe(value => {
countValue = value;
});
</script>

<h1>The count is {countValue}</h1>

<Incrementer/>
<Decrementer/>
<Resetter/>

单击stores.js 选项卡以查看count 的定义。 它是一个可写存储,这意味着它除了订阅之外还有设置和更新方法。

stores.js

1
2
3
import { writable } from 'svelte/store';

export const count = writable(0);

现在转到 Incrementer.svelte 选项卡,以便我们可以连接 + 按钮:
Incrementer.svelte

1
2
3
4
5
6
7
8
9
10
11
<script>
import { count } from './stores.js';

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

<button on:click={increment}>
+
</button>

单击 + 按钮现在应该会更新计数。 对 Decrementer.svelte 执行相反的操作。
Decrementer.svelte

1
2
3
4
5
6
7
8
9
10
<script>
import { count } from './stores.js';
function decrement() {
count.update(n => n - 1);
}
</script>

<button on:click={decrement}>
-
</button>

最后,在Resetter.svelte中,实现reset
Resetter.svelte

1
2
3
4
5
6
7
8
9
10
11
<script>
import { count } from './stores.js';

function reset() {
count.set(0);
}
</script>

<button on:click={reset}>
reset
</button>

二、自动订阅

上一个示例中的应用程序可以运行,但有一个细微的错误-store 已订阅,但从未取消订阅。 如果组件被多次实例化和销毁,这将导致内存泄漏。

首先在 App.svelte 中声明取消订阅:
App.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';

let countValue;

const unsubscribe = count.subscribe(value => {
countValue = value;
});
</script>

<h1>The count is {countValue}</h1>

<Incrementer/>
<Decrementer/>
<Resetter/>

调用 subscribe 方法会返回一个 unsubscribe 函数。

您现在声明了unsubscribe,但仍需要调用它,例如通过 onDestroy 生命周期钩子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
import { onDestroy } from 'svelte';
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';

let countValue;

const unsubscribe = count.subscribe(value => {
countValue = value;
});

onDestroy(unsubscribe);
</script>

不过,它开始有点呆板重复了,尤其是当您的组件订阅了多个存储时。Svelte 给出一个绝佳的替代方案,你可以在 store 名称前面加上$前缀来引用这个store 的值:

1
2
3
4
5
6
7
8
9
<script>
import { count } from './stores.js';
import Incrementer from './Incrementer.svelte';
import Decrementer from './Decrementer.svelte';
import Resetter from './Resetter.svelte';
</script>

<h1>The count is {$count}</h1>

自动订阅仅适用于在组件的顶级范围内声明(或导入)的存储变量。

您也不局限于在标记中使用$count,您还可以在<script>中的任何位置使用它,例如在事件处理程序或反应式声明中。

假设以$开头的任何名称都是指存储值。它实际上是一个保留字符-Svelte将阻止您使用$前缀声明自己的变量。

三、只读 stores

并非所有·stores·都需要在其他地方写入,比如,你可能有一个代表鼠标位置或者用户地理位置的stores,这样的stores从其他地方写入并无意义,对于这种情况,我们有 只读(readable) stores

点击到 stores.js 选项卡, 第一个参数 readable可以一个是个初始值,也可以为 nullundefined ,第二个参数是 start 函数,该函数有个 set 回调方法,并返回一个 stop函数。 当stores首次被subscriber 时调用start函数,stop则是最后当subscriberunsubscribes时调用。

stores.js

1
2
3
4
5
6
7
8
9
10
11
12
import { readable } from 'svelte/store';

export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);

return function stop() {
clearInterval(interval);
};
});

可以通过下面的实例来加深体验和记忆。

REPL

四、派生 Stores

你可以创建一个stores,其内的值可以派生(derived)于一个或多个 其他 stores。在前面的示例的基础上,我们可以创建派生时间到其他页面:来看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { readable, derived } from 'svelte/store';

export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);

return function stop() {
clearInterval(interval);
};
});

const start = new Date();

export const elapsed = derived(
time,
$time => Math.round(($time - start) / 1000)
);

可从多个 store 派生为一个 store,并显式地 set 一个值而不是返回它(这对于异步派生的值很有用处)。有关更多信息,请查阅API 参考。

五、自定义状态

只要一个对象正确地实现了 subscribe 方法,它就是一个 store。除了之外,一切皆有可能。因此,使用特定领域的逻辑来创建自定义store 非常容易。

来看个例子
App

1
2
3
4
5
6
7
8
9
<script>
import { count } from './stores.js';
</script>

<h1>The count is {$count}</h1>

<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>

store.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { writable } from 'svelte/store';

function createCount() {
const { subscribe, set, update } = writable(0);

return {
subscribe,
increment: () => {},
decrement: () => {},
reset: () => {}
};
}

export const count = createCount();

例如, 这个 count store 在前面的例子中包含 incrementdecrementreset组件,以防止暴露 setupdate方法,让我们改写一下:

1
2
3
4
5
6
7
8
9
10
function createCount() {
const { subscribe, set, update } = writable(0);

return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}

六、状态绑定

如果 store可写入的(即具有set方法),则可以绑定其值,就像可以绑定局部组件状态一样。

看例子
App

1
2
3
4
5
6
<script>
import { name, greeting } from './stores.js';
</script>

<h1>{$greeting}</h1>
<input value={$name}>

store.js

1
2
3
4
5
6
import { writable, derived } from 'svelte/store';
export const name = writable('world');
export const greeting = derived(
name,
$name => `Hello ${$name}!`
);

在此示例中,我们有一个可写 store:name和一个派生store :greeting,尝试更改<input>标签:

1
<input bind:value={$name}>

现在,更改name的输入值 ,其值和依赖项都将获得更新。

我们还可以直接分配store值在组件内部,尝试添加<button> 标签:

1
2
3
<button on:click="{() => $name += '!'}">
Add exclamation mark!
</button>

此处 $name += '!' 相当于 name.set($name + '!')

总结

  • 可写状态 writable 是我们常用的,只读状态 readable 是在防止被意外修改的情况下使用。
  • 要读取状态的值,使用状态的 subscribe 方法,当然你最常用的还是使用自动订阅的简写形式,使用 $ 符号
  • 状态派生,可以快速从一个已存在的状态中将其复用。
  • 状态同时也支持绑定。

Svelte 官方入门教程(8) - 状态管理
http://yoursite.com/2022/07/14/svelte-tutorial-8/
作者
昂藏君子
发布于
2022年7月14日
许可协议