
上一讲说的是绑定的内容,今天咱们来看看生命周期
一、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} <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(() => { }); }
|
例如,我们可以在我们的组件初始化时添加一个 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.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.selectionStart
和 this.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