svelte状态管理

Svelte 是没有对应的状态库的,因为它内置了状态管理,它被称为 store

当期望脱离组件的层级(父-子)关系且能够在任意位置都能访问某个状态(变量)时,状态管理仍然是非常有用的一个特性。

总的来说,Svelte 的状态管理更为简单直接 —— 我对这种简单的热爱毫不掩饰,它起码不会使用起来要绕晕脑袋(可能我的理解能力比较低),使得我的代码在别人眼里看来很“高级”。

1、可写状态(Writable stores)

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

在 Svelte 中,我们通过store来实现。

本节的示例会比较复杂,它由一个主(父)组件加上三个子组件组成,之所以分成3个子组件,其目的是为了展示出 store 的特性。

Svelte 将状态划分为两种,一种可读可写,一种只读,都用可读可写(可以读取,也可以修改)虽然省事,不过允许或者说强制状态是只读的,可以防止状态被意外修改。

只读状态会在第3节详尽介绍,本节仅关注可写状态。

要创建一个可读且可写的状态十分简单,例如我们要创建一个数字型的可写状态 count,Svelte 提供 writable 函数来创建:

stores.js

import { writable } from 'svelte/store';

export const count = writable(0);

我们可以将创建状态的代码,放到单独的文件 store.js 中,以便其他需要用到 count 状态的组件可以引入使用。

接下来我们计划要实现的功能是这样:

可写状态示例

非常简单的例子,点击 + 按钮,count 加 1- 按钮,count 减 1reset 按钮重置 count 为 0

首先我们刻意将 +-reset 三个按钮分别写三个组件,这三个按钮在 App.svelte 中是并列出现的,不是父子关系:

Incrementer.svelte (递增)

<script>
  import { count } from './stores.js';

  function increment() {
    // TODO 递增 count 的值,需要用到 count 当前值,应该使用 update
  }
</script>

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

Decrementer.svelte (递减)

<script>
  import { count } from './stores.js';

  function decrement() {
    // TODO 递减 count 的值,需要用到 count 当前值,应该使用 update
  }
</script>

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

Reset.svelte (重置)

<script>
  import { count } from './stores.js';

  function reset() {
    // TODO 重置 count 的值为 0(无需知道当前 count 的值,可以使用 set。
  }
</script>

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

最后,我们将三个组件汇聚 App.svelte:

App.svelte

<script>
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Reset from './Reset.svelte';

  let count_value;

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

<h1>count 当前的值是:{count_value}</h1>

<Incrementer/>
<Decrementer/>
<Reset/>

所谓 store(也即状态),只不过是具有 subscribe 方法的对象,它允许当 store 的值改变时自动通知对此感兴趣的相关组件或程序。在 App.svelte 中,count 便是一个 store,我们在 count.subscribe 的回调中设置 count_value 的值。

点击 stores.js 选项卡看看 count 的定义,可见它是一个 writable store(可写状态),这表示除了 subscribe 方法外,它还具有 set 和 update 方法。

现在转到 Incrementer.svelte 组件,我们可以关联 + 按钮:

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

现在再点击 + 按钮应该会更新 count 了。同理,在 Decrementer.svelte 中实现递减。

最后,在 Reset.svelte 里实现 reset

function reset() {
  count.set(0);
}
当我们需要知道  count 当前值的时候,应该使用  update,它会将当前值传递到回调函数供你使用;如果无需知道,则使用  set

2、自动订阅(Auto-subscriptions)

上一个例子中,程序虽然可以这么写,不过存在一个不易察觉的错误:unsubscribe 函数没有机会被调用。如果该组件会被多次实例化和销毁,这将导致 内存泄露

解决之道,应该使用 onDestroy 这个生命周期 Hook。

<script>
  import { onDestroy } from 'svelte';
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Reset from './Reset.svelte';

  let count_value;

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

  onDestroy(unsubscribe);
</script>

<h1>count 当前的值是:{count_value}</h1>

不过事情又开始变得有点呆板重复了。

特别是当你的组件 subscribe 了很多的 store 的时候。Svelte 给出一个绝佳的替代方案,你可以在 store 名称前面加上$前缀来引用这个 store 的值:

<script>
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Reset from './Reset.svelte';
</script>

<h1>count 当前的值是:{$count}</h1>
自动订阅仅适用于在组件的顶层范围声明(或者导入的JS文件中)的 stroe 变量。

在标记中使用 $count 不会有任何限制,你也可以在 <script> 的任何位置使用它,例如在事件处理程序或者响应式声明中。

Svelte 假定所有以  $ 开头的任何标识符都表示引用某个 store 值,而  $ 实际上是一个保留字符,Svelte 会禁止你使用  $ 作为你声明的变量的前缀。

3、只读状态(Readable stores)

并非所有 store 允许所有人可写的。例如,你可能有一个 store 表示鼠标位置或者用户地理位置,允许 ‘外部’ 来修改这个值是没有意义的。对于这种情况,我们可以用只读store。

本节我们要制作一个数字钟,显示当前的时间,先看看主程序的代码:

App.svelte

<script>
  import { time } from './stores.js';

  const formatter = new Intl.DateTimeFormat('zh-CN', {
    hour12: false,
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });
</script>

<h1>The time is {formatter.format($time)}</h1>

App.svelte 引用了状态文件 stores.js,因此需要编写这份文件:

stores.js

import { readable } from 'svelte/store';

export const time = readable(new Date(), function start(set) {
  // 在此处实现

  return function stop() { };
});

readable 的第一个参数代表初始值,如果还没有具体的初始值,可以先设为 null 或 undefined

第二个参数 start 是一个函数,该函数接受一个 set 回调用于设置值,并且返回一个 stop 函数。

当 store 被第一个订阅者读取时,就会调用 start 函数(然后在 start 函数中使用 set 提供一个最终的值;

最后一个订阅者退订时,调用 stop 函数。

stores.js

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

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

4、状态继承(Derived stores)

你可以调用derived来创建一个新的 store,它将继承自某一个或多个其他的 store。

我们沿用上一节的数字钟例子,稍作添加:

App.svelte

<script>
  import { time } from './stores.js';

  const formatter = new Intl.DateTimeFormat('zh-CN', {
    hour12: false,
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });
</script>

<h1>北京时间:{formatter.format($time)}</h1>

<p>页面已打开 {$elapsed} 秒</p>

stores.js

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 => {} // 稍候在此处填写供订阅的值
);

我们可以创建一个继承自time的 store 来记录页面打开的时间:

export const elapsed = derived(
  time,
  $time => Math.round(($time - start) / 1000)
);
可从多个 store 派生为一个 store,并显式地  set 一个值而不是返回它(这对于异步派生的值很有用处)。有关更多信息,请查阅 API 参考

5、自定义状态(Custom stores)

我们第一节《可写状态》中的例子,创建了3个子组件来实现 +1-1 及 reset 功能。

当然这只是为了演示而写,实际的开发中我们不用真的创建3个子组件,我们会用一个 store 来封装相关的业务:

import { writable } from 'svelte/store';

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

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

export const count = createCount();

App.svelte

<script>
  import { count } from './stores.js';
</script>

<h1>count 的当前值是:{$count}</h1>

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

只要一个对象正确地实现了 subscribe 方法,它即是一个 store。除了之外,怎样都行。因此,使用特定领域的逻辑来创建 store 非常容易。

例如,我们前面例子中的 count store 可以将 incrementdecrement 和 reset 方法包含进来,并避免暴露 set 和 update 两个方法:

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

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

6、状态绑定(Store bindings)

我们假设有一个 store 叫 name,此外再创建一个 greeting 的 store 来继承 name,然后两者都导出:

stores.js

import { writable, derived } from 'svelte/store';

export const name = writable('world');

export const greeting = derived(
  name,
  $name => `Hello ${$name}!`
);

App.svelte

<script>
  import { name, greeting } from './stores.js';
</script>

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

如果 store 是可写的,即它具有 set 方法,则可以绑定到其值,就像绑定到本地组件的状态是一样的。

在这个例子中,我们有一个可写的 store name 和一个派生 store greeting,尝试修改 <input> 元素:

<input bind:value={$name}>

现在修改 input 的 value 会自动更新 name 及其所有相关的依赖项。

我们还可以直接为组件内部的 store 值进行赋值。添加一个 <button> 元素:

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

$name += '!' 赋值是等效于 name.set($name + '!') 的。

7、与 RxJS 结合

如前面所述,要获得 store 的值,是通过 store.subscribe 方法去订阅,store 产生的值会自动推送给订阅者,这点与 RxJS 如出一辙,实际上,你可以将 Svelte 的 store 看作是 RxJS 的 Observable

这样做不是没有原因的,其一是为了更容易与类似 rxjs 这样的库相结合,共同发挥作用;其二是认同 RxJS 这套观察者-订阅的理念且值得借鉴。

下面我们尝试演示 RxJS 相结合的例子:

<script>
  import { fromEvent } from 'rxjs'
  import { map } from 'rxjs/operators'
	
  let position = fromEvent(document, 'mousemove')
	         .pipe(map(e => `鼠标位置: ${e.clientX},${e.clientY}`))
</script>

{$position}

上述例子可见,由 RxJS 产生的 Observable,竟然可以无缝当成 store 来用。

总结

我们通常将状态写到单独的 JS 文件中,例如每个状态一份 JS 文件。

可写状态 writable 应该是我们最常用的状态,只读状态 readable 是在防止被意外修改的情况下使用。

要读取状态的值,使用状态的 subscribe 方法,之所以取这个名字,显然有意为之,例如它兼容 rxjs 的写法,可以轻松与 rxjs 结合使用。

当然,你绝不会放弃使用被称为自动订阅(第2节)的速写形式:使用 $ 符号,有了这个速写形式,可以精简很多状态管理相关的代码,简直爽翻。

自定义状态让你有机会封装一个状态,使其更贴近业务,以及使用起来更为便捷。

Svelte 的状态支持继承,目的是在于可以快速从一个已存在的状态中将其复用。

猜你喜欢

转载自blog.csdn.net/ramblerviper/article/details/124941232
今日推荐