Svelte 官方入门教程(7) - 生命周期

svelte cover

上一讲说的是绑定的内容,今天咱们来看看生命周期

一、onMount

每个组件都有一个生命周期,从创建时开始,到销毁时结束。 有一些函数可以让你在生命周期的关键时刻运行代码。

图片列表示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
let photos = [];
</script>

<style>
.photos {
width: 100%;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 8px;
}

figure, img {
width: 100%;
margin: 0;
}
</style>

<h1>Photo album</h1>

<div class="photos">
{#each photos as photo}
<figure>
<img src={photo.thumbnailUrl} alt={photo.title}>
<figcaption>{photo.title}</figcaption>
</figure>
{:else}
<!-- this block renders when photos.length === 0 -->
<p>loading...</p>
{/each}
</div>

您最常使用的是 onMount,它在组件第一次渲染到 DOM 之后运行。 。前面的章节中,我们已曾粗略了解到它了,还记得我们需要在渲染后与 <canvas> 元素进行交互。

我们将添加一个 onMount 处理程序,通过网络加载一些数据:

1
2
3
4
5
6
7
8
<script>
import { onMount } from 'svelte';
let photos = [];
onMount(async () => {
const res = await fetch(`/tutorial/api/album`);
photos = await res.json();
});
</script>

如果你使用服务端渲染(SSR),建议将 fetch 放入 onMount 中,而不是在 <script> 顶层位置。除了 onDestroy,其余生命周期函数不会在 SSR 运行期间执行,这说明我们可以将某些应该延迟加载的数据,放在组件加载到 DOM 之后获取。

必须在组件初始化时调用生命周期函数,以便回调绑定到组件实例——而不是(比如说)在 setTimeout 中去调用周期函数。

如果 onMount 回调返回一个函数,该函数将在组件被销毁时调用。

1
2
3
4
5
6
7
8
9
<script>
import { onMount } from 'svelte';
onMount(() => {
const interval = setInterval(() => {
console.log('beep');
}, 1000);
return () => clearInterval(interval);
});
</script>

二、onDestroy

要在组件被销毁时运行代码,请使用 onDestroy

下面看例子
App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
import Timer from './Timer.svelte';

let open = false;
let seconds = 0;

const toggle = () => (open = !open);
const handleTick = () => (seconds += 1);
</script>

<div>
<button on:click={toggle}>{open ? 'Close' : 'Open'} Timer</button>
<p>
The Timer component has been open for
{seconds} {seconds === 1 ? 'second' : 'seconds'}
</p>
{#if open}
<Timer callback={handleTick} />
{/if}
</div>

Timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
import { onInterval } from './utils.js';

export let callback;
export let interval = 1000;

onInterval(callback, interval);
</script>

<p>
This component executes a callback every
{interval} millisecond{interval === 1 ? '' : 's'}
</p>

<style>
p {
border: 1px solid blue;
padding: 5px;
}
</style>

utils

1
2
3
4
5
6
7
8
9
import { onDestroy } from 'svelte';

export function onInterval(callback, milliseconds) {
const interval = setInterval(callback, milliseconds);

onDestroy(() => {
// Fix the memory leak here
});
}

例如,我们可以在我们的组件初始化时添加一个 setInterval 函数,并在它不再相关时清理它。 这样做可以防止内存泄漏。

1
2
3
4
5
6
7
8
<script>
import { onDestroy } from 'svelte';

let counter = 0;
const interval = setInterval(() => counter += 1, 1000);

onDestroy(() => clearInterval(interval));
</script>

虽然在组件初始化期间调用生命周期函数很重要,但从哪里调用它们并不重要。 因此,如果我们愿意,我们可以将区间逻辑抽象为 utils.js 中的辅助函数…

1
2
3
4
5
6
7
8
import { onDestroy } from 'svelte';

export function onInterval(callback, milliseconds) {
const interval = setInterval(callback, milliseconds);
onDestroy(() => {
clearInterval(interval);
});
}

然后将util导入我们的组件中:

1
2
3
4
5
<script>
import { onInterval } from './utils.js';
let counter = 0;
onInterval(() => counter += 1, 1000);
</script>

打开和关闭计时器几次,确保计数器持续计时,CPU负载增加。这是由于未删除以前的计时器而导致内存泄漏。在解决示例之前,不要忘记刷新页面。

三、beforeUpdate 和 afterUpdate

beforeUpdate 函数会在 DOM 更新之前运行,而afterUpdate相反则在数据同步到 DOM 之后执行。

它们一前一后,对于强制执行那些仅以状态驱动的方式却难以实现的事情非常有用,例如更新元素的滚动位置。

下方是一个Eliza聊天机器人:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<script>
import Eliza from 'elizabot';
import { beforeUpdate, afterUpdate } from 'svelte';

let div;
let autoscroll;

beforeUpdate(() => {
autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
});

afterUpdate(() => {
if (autoscroll) div.scrollTo(0, div.scrollHeight);
});

const eliza = new Eliza();

let comments = [
{ author: 'eliza', text: eliza.getInitial() }
];

function handleKeydown(event) {
if (event.key === 'Enter') {
const text = event.target.value;
if (!text) return;

comments = comments.concat({
author: 'user',
text
});

event.target.value = '';

const reply = eliza.transform(text);

setTimeout(() => {
comments = comments.concat({
author: 'eliza',
text: '...',
placeholder: true
});

setTimeout(() => {
comments = comments.filter(comment => !comment.placeholder).concat({
author: 'eliza',
text: reply
});
}, 500 + Math.random() * 500);
}, 200 + Math.random() * 200);
}
}
</script>

<style>
.chat {
display: flex;
flex-direction: column;
height: 100%;
max-width: 320px;
}

.scrollable {
flex: 1 1 auto;
border-top: 1px solid #eee;
margin: 0 0 0.5em 0;
overflow-y: auto;
}

article {
margin: 0.5em 0;
}

.user {
text-align: right;
}

span {
padding: 0.5em 1em;
display: inline-block;
}

.eliza span {
background-color: #eee;
border-radius: 1em 1em 1em 0;
}

.user span {
background-color: #0074D9;
color: white;
border-radius: 1em 1em 0 1em;
word-break: break-all;
}
</style>

<div class="chat">
<h1>Eliza</h1>

<div class="scrollable" bind:this={div}>
{#each comments as comment}
<article class={comment.author}>
<span>{comment.text}</span>
</article>
{/each}
</div>

<input on:keydown={handleKeydown}>
</div>

请注意,beforeUpdate 函数需要在组件挂载前运行,所以我们需要先将div与组件标签绑定,并判断div 是否已被正常渲染。

REPL

四、tick

tick函数不同于其他生命周期函数,因为你可以随时调用它,而不用等待组件首次初始化。它返回一个带有resolve方法的 Promise,每当组件pending状态变化便会立即体现到DOM中 (除非没有pending状态变化)。

Svelte中每当组件状态失效时,DOM不会立即更新。 反而Svelte会等待下一个 microtask 以查看是否还有其他变化的状态或组件需要应用更新。这样做避免了浏览器做无用功,使之更高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script>
let text = `Select some text and hit the tab key to toggle uppercase`;

async function handleKeydown(event) {
if (event.key !== 'Tab') return;

event.preventDefault();

const { selectionStart, selectionEnd, value } = this;
const selection = value.slice(selectionStart, selectionEnd);

const replacement = /[a-z]/.test(selection)
? selection.toUpperCase()
: selection.toLowerCase();

text = (
value.slice(0, selectionStart) +
replacement +
value.slice(selectionEnd)
);

// this has no effect, because the DOM hasn't updated yet
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
}
</script>

<style>
textarea {
width: 100%;
height: 200px;
}
</style>

<textarea value={text} on:keydown={handleKeydown}></textarea>

这点在本示例中有所体现。选择文本,然后按“Tab”键。 因为 <textarea> 标签的值已发生变化,浏览器会将选中区域取消选中并将光标置于文本末尾,这显然不是我们想要的,我们可以借助tick函数来解决此问题:

1
import { tick } from 'svelte';

然后在 this.selectionStartthis.selectionEnd 设置结束前立即运行handleKeydown

1
2
3
await tick();
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;

REPL

你可以试试上面的例子然后打开await tick()的注释,然后再试试来感受tick

总结

  • onMount它必须在component初始化期间被调用,如果需要onMount返回一个函数,则在卸载 component 时调用该函数。

  • 首次beforeUpdate回调运行在onMount初始化之前

  • afterUpdate回调函数运行在 component渲染之后

  • onDestroy 它唯一可以运行在服务端渲染(ssr)组件内部。

  • tick 返回一个promise,该promise将在应用state变更后返回resolves,可以类比Vue的tick


Svelte 官方入门教程(7) - 生命周期
http://yoursite.com/2022/07/13/svelte-tutorial-7/
作者
昂藏君子
发布于
2022年7月13日
许可协议