逻辑控制
原生HTML没有逻辑判断,比如条件判断和循环,但是svelte里提供了这些
if判断
如果需要根据某些特定条件渲染某些html,可以用if结构包裹起来
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>
退出登录
</button>
{/if}
{#if !user.loggedIn}
<button on:click={toggle}>
登录
</button>
{/if}
上面使用if判断,根据用户的登录状态来显示不同的html结构
注意if结构的使用方式
{#if 条件}
html结构
{/if}
if-else判断
除了if判断,svelte还提供了else判断,用于简化多个if判断的情况
我们可以把上面的代码改造一下
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>
退出登录
</button>
{:else}
<button on:click={toggle}>
登录
</button>
{/if}
注意else语句的使用方式,是一个单标签,而且是在if标签里面用
{#if 条件}
html结构
{:else}
html结构
{/if}
else if判断
更多种条件的判断可用else if语句
<script>
let x = 7;
</script>
{#if x > 10}
<p>{x} 比10大</p>
{:else if 5 > x}
<p>{x} x小于5</p>
{:else}
<p>{x}在5~10之间</p>
{/if}
else if的使用方式和else一样,同样包裹在if结构里面
Each循环语句
svelte里面也提供了循环语句,Each,更像是vue的v-for和react的map的结合体
<script>
let cats = [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>
<h1>油管上有名的猫猫们</h1>
<ul>
{#each cats as cat}
<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{cat.name}
</a></li>
{/each}
</ul>
注意each语句的使用方式,和if很相似,
{#each 集合 as 个体}
html
{/each}
此外,each语句还有第二个参数 i
{#each cats as cat, i}
<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{i + 1}: {cat.name}
</a></li>
{/each}
这里和vue的v-for对比一下, i的作用相当于index,作为下标
v-for="(item, index) in array"
其实svelte里面提供了解构的用法, 将集合中单个元素的属性解构出来作为第一个参数
{#each cats as { id, name }, i}
<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
{i + 1}: {name}
</a></li>
{/each}
Each循环语句的Key
each有个弊端,默认情况下,当我们修改each的值时,它会在each块的末尾添加或者修改,并且更新任何已经更改的值
看下面这个例子,这里使用了Each循环,渲染了一列表情
我们可以看到,点击“删除第一个”的按钮,它删除了第一个Emoji组件,但是删除了最后一个DOM节点,然后更新其余DOM节点的名称,但不更新表情符号
其具体逻辑如下
- 在子组件中定义好html结构
- 子组件根据传值显示对应的表情内容
- 在父组件里进行修改操作
子组件emojis.svelte
<script>
const emojis = {
apple: "🍎",
banana: "🍌",
carrot: "🥕",
doughnut: "🍩",
egg: "🥚"
}
// name的值根据prop传值的改变而变化
export let name;
// ...但是表情符号的变量在组件初始化时就给定了
const emoji = emojis[name];
</script>
<p>
<span> { name } 的表情是 { emoji }</span>
</p>
<style>
p {
margin: 0.8em 0;
}
span {
display: inline-block;
padding: 0.2em 1em 0.3em;
text-align: center;
border-radius: 0.2em;
background-color: #FFDFD3;
}
</style>
父组件App.svelte
<script>
import Emoji from "./children/Emojis.svelte"
let emojis = [
{ id: 1, name: 'apple' },
{ id: 2, name: 'banana' },
{ id: 3, name: 'carrot' },
{ id: 4, name: 'doughnut' },
{ id: 5, name: 'egg' },
];
function handleClick() {
emojis = emojis.slice(1);
}
</script>
<div>
<button on:click={handleClick}>
删除第一个
</button>
{#each emojis as item}
<Emoji name={item.name}/>
{/each}
</div>
<style>
div{
box-shadow: 0 0 10px 2px rgba(1, 1, 1, .2);
margin: 20px;
padding: 10px;
}
button:hover{
background-color: aqua;
}
</style>
但是我们需要的是移除第一个表情组件和它的DOM节点,而不影响其它的
所以,我们在使用Each的时候要为每个each块指定一个唯一标识“key”
所以,对each循环做一下修改
{#each emojis as item (item.id)}
<Emoji name={item.name}/>
{/each}
上面item.id充当了key,它告诉svelte当组件更新的时候该去改变哪个具体的DOM节点
我们可以使用任何类型的变量充当key,甚至是item本身,但最好用数字或者字符串会更安全
然后,我们想要的效果就出来了
Event事件
事件监听和行内事件
正如我们之前用的那样,在svelte里,可以用on:指令来监听DOM事件
<script>
let m = { x: 0, y: 0 };
function handleMousemove(event) {
m.x = event.clientX;
m.y = event.clientY;
}
</script>
<div on:mousemove={handleMousemove}>
鼠标的实时位置是 {m.x} x {m.y}
</div>
<style>
div { width: 100%; height: 100%; }
</style>
当然,也像原生js样。svelte也支持行内的事件
<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
鼠标的实时位置是 {m.x} x {m.y}
</div>
在某些框架中,您可能会看到出于性能原因避免使用内联事件处理程序的建议,尤其是在循环内部。 该建议不适用于 Svelte — 编译器将始终做正确的事情,无论您选择哪种形式。
事件修饰符
DOM事件有其特定的修饰符来约束其自身的行为,例如,once修饰符,只会执行一次事件
<script>
function handleClick() {
alert('我只会弹出一次')
}
</script>
<button on:click|once={handleClick}>
Click me
</button>
以下是可用的事件修饰符:
preventDefault— 阻止默认事件.stopPropagation— 阻止事件冒泡passive— 提升鼠标滚轮事件以及触摸事件的性能nonpassive— 显式的设置passive为falsecapture— 在事件捕获阶段触发事件处理函数once—让事件只触发一次self— 只有事件源是元素本身的时候才触发
如果需要多个修饰符,我们可以链式调用
on:click|once|capture={...}.
组件事件
子组件可以派发事件到父组件,不过必须先创建一个事件派发器,这是svelte的内置函数,可以直接引入使用
新建一个子组件Inner.svelte
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: '你好'
});
}
</script>
<button on:click={sayHello}>
点我说你好
</button>
在父组件App.svelte中接收子组件派发的meaaage事件
<script>
import Inner from './children/Inner.svelte';
function handleMessage(event) {
console.log(event)
alert(event.detail.text);
}
</script>
<Inner on:message={handleMessage}/>
点击按钮触发的效果如下:
这里要注意几点:
- 子组件的
dispatch(eventName, {})函数的第一个参数就是向父组件派发的事件名称,第二个参数是派发的数据 - 父组件监听时的事件名称要和子组件派发的一致
createEventDispatcher必须在组件第一次实例化时调用——你不能稍后在内部执行它,例如 一个 setTimeout 回调或者promise中。- 如果父组件不接受派发的事件,子组件依然可以派发成功,但不会有任何反应
组件嵌套的事件转发
与 DOM 事件不同,组件事件不会冒泡。 如果要监听某个深度嵌套组件上的事件,中间组件必须转发该事件
在下面的案例中,我们把Inner组件包裹在新的Outer.svelte组件中
这样,Inner组件的事件先派发给Outer,Outer再派发给App
所以我们要在Outer组件中再使用createEventDispatcher创建一个派发器,将来自Inner组件的事件派发给App
Outer.svelte
<script>
import Inner from './Inner.svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function forward(event) {
dispatch('message', event.detail);
}
</script>
<Inner on:message={forward}/>
但这样,当事件很多的时候,代码就比较冗余
不过svelte给我们提供了简写
没有值的事件监听意味着派发所有的事件
<script>
import Inner from './Inner.svelte';
</script>
<Inner on:message/>
上面的代码也就是说:监听Inner派发来的事件的时候不给处理函数,就会直接派发来自Inner的所有事件
所以我们在App.svelte里就可以拿到从Inner组件里面派发的事件了
App.svelte
<script>
import Outer from './Outer.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Outer on:message={handleMessage}/>
原生DOM事件转发
事件转发也适用于 DOM 事件。
我们希望在我们的 CustomButton组件 上获得点击通知——为此,我们只需要在 CustomButton.svelte 中的 元素上转发点击事件
CustomButton.svelte
<button on:click>
点我
</button>
App.svelte
<script>
import CustomButton from './CustomButton.svelte';
function handleClick() {
alert('按钮被点了');
}
</script>
<CustomButton on:click={handleClick}/>
组件生命周期
每个组件都有一个生命周期,从创建时开始,到销毁时结束。
我们可以在关键的声明周期函数中去处理我们特定的逻辑
onMount
最常使用的是 onMount,它在组件第一次渲染到 DOM 之后运行。
下面这个例子,我们在onMount时来加载一些网络图片
生命周期函数是需要单独引入的
<script>
import { onMount } from 'svelte';
let photos = [];
onMount(async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`);
photos = await res.json();
});
</script>
生命周期函数必须在组件初始化时调用,以便回调绑定到组件实例——而不是(比如)在 setTimeout 中。
如果 onMount 回调返回一个函数,则该函数将在组件销毁时调用。
推荐将网络数据请求的行为放在onMount中,这样减少在ODM加载完成后还出现数据未加载的情况
<script>
import { onMount } from 'svelte';
let photos = [];
onMount(async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`);
photos = await res.json();
});
</script>
<div class="photos">
<h1>Photo album</h1>
{#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>
<style>
div{
box-shadow: 0 0 10px 2px rgba(1, 1, 1, .2);
margin: 20px;
padding: 10px;
}
</style>
onDestory
要在组件销毁时运行代码,请使用 onDestroy。
例如,我们可以在组件初始化时添加一个 setInterval 函数,并在它不再相关时将其清理干净。 这样做可以防止内存泄漏。
虽然在组件初始化期间调用生命周期函数很重要,但从哪里调用它们并不重要。
因此,如果我们愿意,我们可以将处理组件销毁时的逻辑抽象为 utils.js 中的辅助函数...
新建utils.js,这里在组件销毁时我们清理掉定时器
import { onDestroy } from 'svelte';
export function onInterval(callback, milliseconds) {
const interval = setInterval(callback, milliseconds);
onDestroy(() => {
clearInterval(interval);
console.log("定时器已经清理,组件销毁")
});
}
在 Timer.svelte中引入
<script>
import { onInterval } from './utils.js';
export let callback;
export let interval = 1000;
onInterval(callback, interval);
</script>
<p>
组件每{interval} 秒执行一次回调函数
</p>
<style>
p {
border: 1px solid blue;
padding: 5px;
}
</style>
在App.svelte中使用Timer.svelte
我们使用一个按钮来控制定时器的打开和关闭,进而控制Timer组件的挂载与销毁
这样可以监控onDestory生命周期对定时器的清除操作
<script>
import Timer from './Timer.svelte';
let open = true;
let seconds = 0;
const toggle = () => (open = !open);
const handleTick = () => (seconds += 1);
</script>
<div>
<button on:click={toggle}>{open ? '关闭定时器' : '打开定时器'} </button>
<p>
定时器组件已经运行了
{seconds} 次
</p>
{#if open}
<Timer callback={handleTick} />
{:else}
<p>组件销毁</p>
{/if}
</div>
打开和关闭计时器几次,并确保计数器保持滴答作响并且 CPU 负载增加。 这是由于内存泄漏,因为之前的计时器没有被删除。 在解决示例之前不要忘记刷新页面。
通常,在onDestory周期中,我们可以做以下操作
- 清除定时器、计时器等任务
- 销毁一些图表
- 清除还在执行的动画
- 清除占用内存较大的变量
beforeUpdate 和 afterUpdate
beforeUpdate函数在DOM更新前就会立即执行,而afterUpdate则恰好相反,在DOM更新后执行,用于DOM和数据同步后才处理的逻辑
通常,这两个函数用于强制执行无法用状态改变而驱动的事情,例如更新元素滚动的位置
这里我们使用了一个聊天机器人的案例,根据输入的内容,控制聊天窗口的滚动
聊天机器人引用了第三方的库 ElizaBot,具体用法可以参考其文档
执行npm install elizaBot即可安装该库
这里用它作为一个自动回复的机器人,来和我们聊天
新建一个Chat.svelte
<script>
import Eliza from 'elizabot';
import { beforeUpdate, afterUpdate } from 'svelte';
let div;
let autoscroll;
beforeUpdate(() => {
autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
console.log("滚动条件具备")
});
afterUpdate(() => {
if (autoscroll) div.scrollTo(0, div.scrollHeight);
console.log("滚动完毕")
});
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>
<div class="wrap">
<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>
</div>
<style>
.wrap{
box-shadow: 0 0 10px 2px rgba(1, 1, 1, .2);
margin: 20px;
padding: 10px;
}
.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>
上面的代码可以直接拷贝运行,不必在意里面具体写的什么,我们只需要看看beforeUpdate 和 afterUpdate函数触发的时机和效果
beforeUpdate(() => {
autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
console.log("滚动条件具备")
});
afterUpdate(() => {
if (autoscroll) div.scrollTo(0, div.scrollHeight);
console.log("滚动完毕")
});
代码有几个特殊的地方,在后面会讲到
<div class="scrollable" bind:this={div}>
</div>
上面的bind:this={div},可以理解为将该真实的DOM元素绑定到一个变量上(参考vue和react的ref)
当我们输入内容回车时。机器人先回复“...”,随后再回复我们的问题
然后可以看到控制台输出的内容
聊天内容增加时。div就会自动往上滚动,这个时候就会触发我们的生命周期函数
第一组是组件挂载时触发的
第二组是我们发送消息,机器人回复...触发的
第三组是机器人回复我们问题后触发的
tick
tick 函数与其他生命周期函数不同,您可以随时调用它,而不仅仅是在组件第一次初始化时调用。
它返回一个Promise,一旦任何挂起的状态更改应用于 DOM(或立即,如果没有挂起的状态更改),它就会解决
当您在 Svelte 中更新组件状态时,它不会立即更新 DOM。
相反,它会等到下一个微任务来查看是否有任何其他需要应用的更改,包括其他组件中的更改。
这样做可以避免不必要的工作,并允许浏览器更有效地进行批处理。
详解js的宏任务与微任务(event loop) www.jianshu.com/p/53e8597df…
js 宏任务和微任务 www.cnblogs.com/wangziye/p/…
新建一个Tick.svelte
<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键
可以看到文本被清除,鼠标光标自动跳转的文本的最后面
然而这不是我们想要的效果,我们希望按下Tab键后选中的文本不被清除,同时文本变成大写,光标位置也不发生变化
我们可以通过导入tick来解决这个问题...
在selectionStart和selectionEnd赋值前,调用tick函数
import { tick } from 'svelte';
await tick();
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
然后我们再选中按下Tab键,就可以看到想要的效果了
上面代码有个特别的地方
const { selectionStart, selectionEnd, value } = this;
注意这里的this,不是我们理解的那个this,而是textarea这个DOM元素本身