云驹博客

路漫漫其修远兮,吾将上下而求索。

0%

Vue组件slot(插槽)和插槽作用域

插槽 slot

2.6.0 版本中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slotslot-scope 这两个目前已被废弃但未被移除且仍在文档中的特性。新语法的由来可查阅这份 RFC

插槽内容

Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot> 元素作为承载分发内容的出口。

它允许你像这样合成组件:

1
<navigation-link url="/profile"> Your Profile </navigation-link>

然后你在 <navigation-link> 的模板中可能会写为:

1
2
3
<a v-bind:href="url" class="nav-link">
<slot></slot>
</a>

当组件渲染的时候,<slot> 将会被替换为“Your Profile”。插槽内可以包含任何模板代码,包括 HTML:

1
2
3
4
5
<navigation-link url="/profile">
<!-- 添加一个 Font Awesome 图标 -->
<span class="fa fa-user"></span>
Your Profile
</navigation-link>

甚至其它的组件:

1
2
3
4
5
<navigation-link url="/profile">
<!-- 添加一个图标的组件 -->
<font-awesome-icon name="user"></font-awesome-icon>
Your Profile
</navigation-link>

如果 <navigation-link> 没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

了解更多

编译作用域

当你想在一个插槽中使用数据时,例如:

1
<navigation-link url="/profile"> Logged in as {{ user.name }} </navigation-link>

该插槽跟模板的其它地方一样可以访问相同的实例属性 (也就是相同的“作用域”),而不能访问 <navigation-link> 的作用域。例如 url 是访问不到的:

1
2
3
4
5
6
7
8
<navigation-link url="/profile">
Clicking here will send you to: {{ url }}
<!--
这里的 `url` 会是 undefined,因为 "/profile" 是
_传递给_ <navigation-link> 的而不是
在 <navigation-link> 组件*内部*定义的。
-->
</navigation-link>

作为一条规则,请记住:

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

了解更多

后备内容

有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 <submit-button> 组件中:

1
2
3
<button type="submit">
<slot></slot>
</button>

我们可能希望这个 <button> 内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在 <slot> 标签内:

1
2
3
<button type="submit">
<slot>Submit</slot>
</button>

现在当我在一个父级组件中使用 <submit-button> 并且不提供任何插槽内容时:

1
<submit-button></submit-button>

后备内容“Submit”将会被渲染:

1
<button type="submit">Submit</button>

但是如果我们提供内容:

1
<submit-button> Save </submit-button>

则这个提供的内容将会被渲染从而取代后备内容:

1
<button type="submit">Save</button>

了解更多

具名插槽

2.6.0 起有所更新。已废弃的使用 slot 特性的语法在这里

有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout> 组件:

1
2
3
4
5
6
7
8
9
10
11
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
</footer>
</div>

对于这样的情况,<slot> 元素有一个特殊的特性:name。这个特性可以用来定义额外的插槽:

1
2
3
4
5
6
7
8
9
10
11
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>

一个不带 name<slot> 出口会带有隐含的名字“default”。

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

1
2
3
4
5
6
7
8
9
10
11
12
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>

现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot<template> 中的内容都会被视为默认插槽的内容。

然而,如果你希望更明确一些,仍然可以在一个 <template> 中包裹默认插槽的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>

任何一种写法都会渲染出:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>

注意 v-slot 只能添加在一个 <template> (只有一种例外情况),这一点和已经废弃的 slot 特性不同。

具名插槽

作用域插槽

自 2.6.0 起有所更新。已废弃的使用 slot-scope 特性的语法在这里

有时让插槽内容能够访问子组件中才有的数据是很有用的。例如,设想一个带有如下模板的 <current-user> 组件:

1
2
3
<span>
<slot>{{ user.lastName }}</slot>
</span>

我们想让它的后备内容显示用户的名,以取代正常情况下用户的姓,如下:

1
<current-user> {{ user.firstName }} </current-user>

然而上述代码不会正常工作,因为只有 <current-user> 组件可以访问到 user 而我们提供的内容是在父级渲染的。

为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个特性绑定上去:

1
2
3
<span>
<slot v-bind:user="user"> {{ user.lastName }} </slot>
</span>

绑定在 <sloat> 元素上的特性被称为插槽 prop。现在在父级作用域中,我们可以给 v-slot 带一个值来定义我们提供的插槽 prop 的名字:

1
2
3
4
5
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>

在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps,但你也可以使用任意你喜欢的名字。

了解更多

独占默认插槽的缩写语法

在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:

1
2
3
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>

这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:

1
<current-user v-slot="slotProps"> {{ slotProps.user.firstName }} </current-user>

注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:

1
2
3
4
5
6
7
<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</current-user>

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template>的语法:

1
2
3
4
5
6
7
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>

<template v-slot:other="otherSlotProps"> ... </template>
</current-user>

了解更多

解构插槽 Prop

作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:

1
2
3
4
function (slotProps) {
// 插槽内容
}

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以在支持的环境下 (单文件组件现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:

1
<current-user v-slot="{ user }"> {{ user.firstName }} </current-user>

这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person

1
<current-user v-slot="{ user: person }"> {{ person.firstName }} </current-user>

你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:

1
2
3
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>

了解更多

动态插槽名

2.6.0 新增

动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:

1
2
3
<base-layout>
<template v-slot:[dynamicSlotName]> ... </template>
</base-layout>

了解更多

具名插槽的缩写

v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header

1
2
3
4
5
6
7
8
9
10
11
12
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>

<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template #footer>
<p>Here's some contact info</p>
</template>
</base-layout>

然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:

1
2
<!-- 这样会触发一个警告 -->
<current-user #="{ user }"> {{ user.firstName }} </current-user>

如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:

1
<current-user #default="{ user }"> {{ user.firstName }} </current-user>

了解更多

其它示例

插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的。

例如,我们要实现一个 <todo-list> 组件,它是一个列表且包含布局和过滤逻辑:

1
2
3
<ul>
<li v-for="todo in filteredTodos" v-bind:key="todo.id">{{ todo.text }}</li>
</ul>

我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo 作为一个插槽 prop 进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
<ul>
<li v-for="todo in filteredTodos" v-bind:key="todo.id">
<!--
我们为每个 todo 准备了一个插槽,
将 `todo` 对象作为一个插槽的 prop 传入。
-->
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>

现在当我们使用 <todo-list> 组件的时候,我们可以选择为 todo 定义一个不一样的 <template> 作为替代方案,并且可以从子组件获取数据:

1
2
3
4
5
6
<todo-list v-bind:todos="todos">
<template v-slot:todo="{ todo }">
<span v-if="todo.isComplete"></span>
{{ todo.text }}
</template>
</todo-list>

这只是作用域插槽用武之地的冰山一角。想了解更多现实生活中的作用域插槽的用法,我们推荐浏览诸如 Vue Virtual ScrollerVue PromisedPortal Vue 等库。

了解更多

过度动画 transition

css 动画库 animate.css

JavaScript 动画库 Velocity.js

概念

1. 实现条件:在需要有过渡效果的标签外面添加<transition>。也就是说 vue 中被<transition>包裹的元素才能实现过渡效果。

2. 注意
类名的说明:对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>,则 v- 是这些类名的默认前缀。如果你使用了 <transition name="my-transition">,那么 v-enter 会替换为 my-transition-enter
opacity 的说明:在过渡动画的显示和隐藏会用 opacity 属性来表征。opacity: 1 不透明(显示)| opacity: 0 透明(不显示)

3. 原理
vue 在加了<transition>标签的元素提供了三个进入过渡的类,和三个离开过渡的类。

进入过渡 离开过渡
.v-enter:定义进入过渡的开始状态 .v-leave:定义离开过渡的开始状态
.v-enter-to:定义进入过渡的结束状态 .v-leave-to:定义离开过渡的结束状态
.v-enter-active:定义进入过渡生效时的状态 .v-leave-active:定义离开过渡生效时的状态

4. 这 6 个类的生效时间
对于进入动画来说:

.v-enter . v-enter-to . v-enter-active
定义过渡的开始状态 定义过渡的结束状态 定义过渡生效时的状态
插入之前生效 插入之后下一帧生效(同时 v-enter 被移除) 动画整个过程生效
下一帧被移除 动画完成之后移除 动画整个过程生效

对于离开动画来说:

.v-leave .v-leave-to .v-leave-active
定义过渡的开始状态 定义过渡的结束状态 定义过渡生效时的状态
离开过渡被触发时立刻生效 触发之后下一帧生效 (与此同时 v-leave 被删除) 整个离开过渡的阶段中应用
下一帧被移除 动画完成之后移除 整个离开过渡的阶段中应用

5. 原理图

img

代码示例

定义一个长方体 div,在点击按钮显示时,让高度从 0 增长到 200px。切换隐藏时,让高度从 200px 减少到 0.

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
//一个命名为fade的<transition>标签包裹着类名为h的<div>
<transition name="fade">
<div class="h" v-if="show">hello world</div>
</transition>
<style>

/* div的初始状态*/
.h {
width:100px;
height: 200px;
background-color: aqua;
}

-------------------------------------------------------------------------------------

/**
* 定义进入动画和退出动画的过程
* 代表关注的是height的变化,动画的时间是5
*/
.fade-enter-active, .fade-leave-active {
transition: height 5s;
}

-------------------------------------------------------------------------------------

/* 定义进入动画的初始状态*/
.fade-enter {
height: 0;
}

/* 定义进入动画的结束状态*/
.fade-enter-to {
height: 200px;
}
-------------------------------------------------------------------------------------

/* 定义离开动画的初始状态*/
.fade-leave {
height: 200px;
}

/* 定义离开动画的结束状态*/
.fade-leave-to {
height: 0;
}
</style>

了解更多

多个元素过渡

我们之后讨论多个组件的过渡,对于原生标签可以使用 v-if/v-else 。最常见的多标签过渡是一个列表和描述这个列表为空消息的元素:

1
2
3
4
5
6
<transition>
<table v-if="items.length > 0">
<!-- ... -->
</table>
<p v-else>Sorry, no items found.</p>
</transition>

可以这样使用,但是有一点需要注意:

当有相同标签名的元素切换时,需要通过 key 特性设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 <transition> 组件中的多个元素设置 key 是一个更好的实践。

示例:

1
2
3
4
<transition>
<button v-if="isEditing" key="save">Save</button>
<button v-else key="edit">Edit</button>
</transition>

在一些场景中,也可以通过给同一个元素的 key 特性设置不同的状态来代替 v-ifv-else,上面的例子可以重写为:

1
2
3
<transition>
<button v-bind:key="isEditing">{{ isEditing ? 'Save' : 'Edit' }}</button>
</transition>

使用多个 v-if 的多个元素的过渡可以重写为绑定了动态属性的单个元素过渡。例如:

1
2
3
4
5
<transition>
<button v-if="docState === 'saved'" key="saved">Edit</button>
<button v-if="docState === 'edited'" key="edited">Save</button>
<button v-if="docState === 'editing'" key="editing">Cancel</button>
</transition>

可以重写为:

1
2
3
4
5
6
<transition>
<button v-bind:key="docState">{{ buttonMessage }}</button>
</transition>
// ... computed: { buttonMessage: function () { switch (this.docState) { case
'saved': return 'Edit' case 'edited': return 'Save' case 'editing': return
'Cancel' } } }

了解更多

过渡模式

在两个元素的过渡中,两个元素都被重绘了,一个离开过渡的时候另一个开始进入过渡。这是 <transition> 的默认行为 - 进入和离开同时发生。

解决方法:

  • 需要过渡的元素加上绝对定位,绝对定位在彼此之上的时候运行正常

  • 加上 css: translate 过渡像滑动过渡

  • Vue 提供了 过渡模式强烈建议

    • in-out:新元素先进行过渡,完成之后当前元素过渡离开。

    • out-in:当前元素先进行过渡,完成之后新元素过渡进入。

      示例:

      1
      2
      3
      <transition name="fade" mode="out-in">
      <!-- ... the buttons ... -->
      </transition>

组件过渡

多个组件的过渡简单很多 - 我们不需要使用 key 特性。相反,我们只需要使用动态组件

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>组件过渡</title>
<style>
.component-fade-enter-active,
.component-fade-leave-active {
transition: opacity 0.3s ease;
}

.component-fade-enter,
.component-fade-leave-to

/* .component-fade-leave-active for below version 2.1.8 */ {
opacity: 0;
}
</style>
<script src="/lib/vue/dist/vue.js"></script>
</head>
<body>
<div id="transition-components-demo">
<button @click="view = 'v-a'">a</button>
<button @click="view = 'v-b'">b</button>
<transition name="component-fade" mode="out-in">
<component v-bind:is="view"></component>
</transition>
</div>
<script>
new Vue({
el: "#transition-components-demo",
data: {
view: "v-a",
},
components: {
"v-a": {
template: "<div>Component A</div>",
},
"v-b": {
template: "<div>Component B</div>",
},
},
});
</script>
</body>
</html>

了解更多

列表过度

列表的进入/离开过渡

现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS 类名。

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>列表过度</title>
<style>
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active,
.list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
</style>
<script src="/lib/vue/dist/vue.js"></script>
</head>
<body>
<div id="list-demo" class="demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>

<script>
new Vue({
el: "#list-demo",
data: {
items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
nextNum: 10,
},
methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.items.length);
},
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++);
},
remove: function () {
this.items.splice(this.randomIndex(), 1);
},
},
});
</script>
</body>
</html>

这个例子有个问题,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。

了解更多

列表的排序过渡

<transition-group> 组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move 特性,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 name 属性来自定义前缀,也可以通过 move-class 属性手动设置。

示例 1:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>列表过度</title>
<style>
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active,
.list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
/* 新增strat */
.list-leave-active {
position: absolute;
}
.list-move {
transition: all 1s;
}
/* end */
</style>
<script src="/lib/vue/dist/vue.js"></script>
</head>
<body>
<div id="list-demo" class="demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>

<script>
new Vue({
el: '#list-demo',、
data: {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10
},
methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.items.length)
},
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++)
},
remove: function () {
this.items.splice(this.randomIndex(), 1)
},
}
})
</script>
</body>
</html>

示例 2:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>列表过度-交错过渡</title>
<link rel="stylesheet" href="/lib/animate/animate.css" />
<style>
.abs {
position: absolute;
}
.myAni {
transition: all 1s;
}
li {
display: table;
}
</style>
<script src="/lib/vue/dist/vue.js"></script>
</head>
<body>
<div id="box">
<input type="text" v-model="text" />
<transition-group
tag="ul"
enter-active-class="animated fadeInLeft"
leave-active-class="animated fadeOutLeft abs"
move-class="myAni"
>
<li v-for="data in querylist" :key="data">{{ data }}</li>
</transition-group>
<p>{{ querylist }}</p>
</div>

<script>
new Vue({
el: "#box",
data: {
text: "",
list: [
"asdfffffsafsdf",
"asdjfklasjdflk",
"fjkfjklsjfkdaf",
"fdjslfjlkasfsf",
"ffjlajsdfoisff",
],
},
computed: {
querylist: function () {
return this.list.filter(
(item) =>
item.toLowerCase().indexOf(this.text.toLowerCase()) !== -1
);
},
},
});
</script>
</body>
</html>

需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline 。作为替代方案,可以设置为 display: inline-block 或者放置于 flex 中

了解更多

把过渡放到组件里

管理太多的状态过渡会很快的增加 Vue 实例或者组件的复杂性,幸好很多的动画可以提取到专用的子组件。
我们来将之前的示例改写一下:

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
<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>

<div id="example-8">
<input v-model.number="firstNumber" type="number" step="20" /> +
<input v-model.number="secondNumber" type="number" step="20" /> = {{ result }}
<p>
<animated-integer v-bind:value="firstNumber"></animated-integer> +
<animated-integer v-bind:value="secondNumber"></animated-integer> =
<animated-integer v-bind:value="result"></animated-integer>
</p>
</div>

<script>
// 这种复杂的补间动画逻辑可以被复用
// 任何整数都可以执行动画
// 组件化使我们的界面十分清晰
// 可以支持更多更复杂的动态过渡
// 策略。
Vue.component("animated-integer", {
template: "<span>{{ tweeningValue }}</span>",
props: {
value: {
type: Number,
required: true,
},
},
data: function () {
return {
tweeningValue: 0,
};
},
watch: {
value: function (newValue, oldValue) {
this.tween(oldValue, newValue);
},
},
mounted: function () {
this.tween(0, this.value);
},
methods: {
tween: function (startValue, endValue) {
var vm = this;
function animate() {
if (TWEEN.update()) {
requestAnimationFrame(animate);
}
}

new TWEEN.Tween({ tweeningValue: startValue })
.to({ tweeningValue: endValue }, 500)
.onUpdate(function () {
vm.tweeningValue = this.tweeningValue.toFixed(0);
})
.start();

animate();
},
},
});

// 所有的复杂度都已经从 Vue 的主实例中移除!
new Vue({
el: "#example-8",
data: {
firstNumber: 20,
secondNumber: 40,
},
computed: {
result: function () {
return this.firstNumber + this.secondNumber;
},
},
});
</script>

我们能在组件中结合使用这一节讲到各种过渡策略和 Vue 内建的过渡系统。总之,对于完成各种过渡动效几乎没有阻碍。

了解更多

自定义指令

指令定义

全局指令:

1
2
3
4
5
6
7
8
// 注册一个全局自定义指令 `v-focus`
Vue.directive("focus", {
// 当被绑定的元素插入到 DOM 中时执行
inserted: function (el) {
// 聚焦元素
el.focus();
},
});

局部指令:

1
2
3
4
5
6
7
8
9
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用。

钩子函数参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM 。
  • binding:一个对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

示例:

html:

1
<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>

js:

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
Vue.directive("demo", {
bind: function (el, binding, vnode) {
var s = JSON.stringify;
el.innerHTML =
"name: " +
s(binding.name) +
"<br>" +
"value: " +
s(binding.value) +
"<br>" +
"expression: " +
s(binding.expression) +
"<br>" +
"argument: " +
s(binding.arg) +
"<br>" +
"modifiers: " +
s(binding.modifiers) +
"<br>" +
"vnode keys: " +
Object.keys(vnode).join(", ");
},
});

new Vue({
el: "#hook-arguments-example",
data: {
message: "hello!",
},
});
结果:
name:”demo”
value:”hello!”
expression:”message”
argument:”foo”
modifiers:{“a”:true,”b”:true}
vnode keys:tag,data,children,text,elm,ns,context,fnContext,fnOptions,fnScopeld,key, componentOptions,componentinstance,parent,raw,isStatic,isRootlnsert,isComment, isCloned,isOnce,asyncFactory,asyncMeta,isAsyncPlaceholder

动态指令参数

示例:

html:

1
2
3
4
<div id="dynamicexample">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>

js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.directive("pin", {
bind: function (el, binding, vnode) {
el.style.position = "fixed";
var s = binding.arg == "left" ? "left" : "top";
el.style[s] = binding.value + "px";
},
});

new Vue({
el: "#dynamicexample",
data: function () {
return {
direction: "left",
};
},
});

函数简写

在很多时候,你可能想在 bindupdate 时触发相同行为,而不关心其它的钩子。比如这样写:

1
2
3
Vue.directive("color-swatch", function (el, binding) {
el.style.backgroundColor = binding.value;
});

对象字面量

如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。

1
2
3
4
5
6
7
8
<div v-demo="{ color: 'white', text: 'hello!' }"></div>

<script>
Vue.directive("demo", function (el, binding) {
console.log(binding.value.color); // => "white"
console.log(binding.value.text); // => "hello!"
});
</script>

了解更多

过滤器

过滤器可以用在两个地方:

  • 双花括号插值
  • v-bind 表达式 (从 2.1.0+ 版本后开始支持)

示例

全局过滤器:

1
2
3
4
5
6
7
8
9
Vue.filter("capitalize", function (value) {
if (!value) return "";
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
});

new Vue({
// ...
});

组件内定义过滤器:

1
2
3
4
5
6
7
8
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}

过滤器串联/传参

示例:

1
{{ message | filterA | filterB }}

在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。

过滤器是 JavaScript 函数,因此可以接收参数:

1
{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。

路由 router

接下来我们可以了解下更多关于 <router-link> 的属性。

to

表示目标路由的链接。 当被点击后,内部会立刻把 to 的值传到 router.push(),所以这个值可以是一个字符串或者是描述目标位置的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 字符串 -->
<router-link to="home">Home</router-link>
<!-- 渲染结果 -->
<a href="home">Home</a>

<!-- 使用 v-bind 的 JS 表达式 -->
<router-link v-bind:to="'home'">Home</router-link>

<!-- 不写 v-bind 也可以,就像绑定别的属性一样 -->
<router-link :to="'home'">Home</router-link>

<!-- 同上 -->
<router-link :to="{ path: 'home' }">Home</router-link>

<!-- 命名的路由 -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

<!-- 带查询参数,下面的结果为 /register?plan=private -->
<router-link :to="{ path: 'register', query: { plan: 'private' }}"
>Register</router-link
>

replace

设置 replace 属性的话,当点击时,会调用 router.replace() 而不是router.push(),导航后不会留下 history 记录。

1
<router-link :to="{ path: '/abc'}" replace></router-link>

append

设置 append 属性后,则在当前 (相对) 路径前添加基路径。例如,我们从 /a 导航到一个相对路径 b,如果没有配置 append,则路径为 /b,如果配了,则为 /a/b

1
<router-link :to="{ path: 'relative/path'}" append></router-link>

tag

有时候想要 <router-link> 渲染成某种标签,例如 <li>。 于是我们使用 tagprop 类指定何种标签,同样它还是会监听点击,触发导航。

1
2
3
<router-link to="/foo" tag="li">foo</router-link>
<!-- 渲染结果 -->
<li>foo</li>

active-class

设置 链接激活时使用的 CSS 类名。可以通过以下代码来替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
<style>
._active {
background-color: red;
}
</style>
<p>
<router-link v-bind:to="{ path: '/route1'}" active-class="_active"
>Router Link 1</router-link
>
<router-link v-bind:to="{ path: '/route2'}" tag="span"
>Router Link 2</router-link
>
</p>

注意这里 class 使用 **active_class=”_active”**。

exact-active-class

配置当链接被精确匹配的时候应该激活的 class。可以通过以下代码来替代。

1
2
3
4
5
6
7
8
<p>
<router-link v-bind:to="{ path: '/route1'}" exact-active-class="_active"
>Router Link 1</router-link
>
<router-link v-bind:to="{ path: '/route2'}" tag="span"
>Router Link 2</router-link
>
</p>

event

声明可以用来触发导航的事件。可以是一个字符串或是一个包含字符串的数组。

1
2
3
<router-link v-bind:to="{ path: '/route1'}" event="mouseover"
>Router Link 1</router-link
>