0%

状态过渡

Vue 除了可以对元素或组件提供进入、离开和列表的动效. 还可以将数值结合 Vue 的响应式和组件系统, 使用第三方库来实现切换元素的过渡状态

动态动画与侦听器

我们可以通过侦听器监听到数值属性的数值更新, 当数值更新时, 就会触发动画

1
2
3
4
5
6
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>

<div id="animated-number-demo">
<input v-model.number="number" type="number" step="20" />
<p>{{ animatedNumber }}</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Vue({
el: '#animated-number-demo',
data: {
number: 0,
tweenedNumber: 0
},
computed: {
animatedNumber: function() {
return this.tweenedNumber.toFixed(0)
}
},
watch: {
number: function(newValue) {
TweenLite.to(this.$data, 0.5, { tweenedNumber: newValue })
}
}
})

对于不能直接像数字一样存储的值,比如 CSS 中的 color 的值,通过下面的例子我们来通过 Tween.js 和 Color.js 实现一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<script src="https://cdn.jsdelivr.net/npm/color-js@1.0.3"></script>

<div id="example-7">
<input
v-model="colorQuery"
v-on:keyup.enter="updateColor"
placeholder="Enter a color"
/>
<button v-on:click="updateColor">Update</button>
<p>Preview:</p>
<span
v-bind:style="{ backgroundColor: tweenedCSSColor }"
class="example-7-color-preview"
></span>
<p>{{ tweenedCSSColor }}</p>
</div>
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
var Color = net.brehaut.Color

new Vue({
el: '#example-7',
data: {
colorQuery: '',
color: {
red: 0,
green: 0,
blue: 0,
alpha: 1
},
tweenedColor: {}
},
created: function() {
this.tweenedColor = Object.assign({}, this.color)
},
watch: {
color: function() {
function animate() {
if (TWEEN.update()) {
requestAnimationFrame(animate)
}
}

new TWEEN.Tween(this.tweenedColor).to(this.color, 750).start()

animate()
}
},
computed: {
tweenedCSSColor: function() {
return new Color({
red: this.tweenedColor.red,
green: this.tweenedColor.green,
blue: this.tweenedColor.blue,
alpha: this.tweenedColor.alpha
}).toCSS()
}
},
methods: {
updateColor: function() {
this.color = new Color(this.colorQuery).toRGB()
this.colorQuery = ''
}
}
})
1
2
3
4
5
.example-7-color-preview {
display: inline-block;
width: 50px;
height: 50px;
}

动态状态过渡

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

1
2
3
4
5
6
7
8
9
10
11
<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>
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
// 这种复杂的补间动画逻辑可以被复用
// 任何整数都可以执行动画
// 组件化使我们的界面十分清晰
// 可以支持更多更复杂的动态过渡
// 策略。
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
}
}
})

概述

Vue 在插入、更新或者移出 DOM 时, 提供多种不同方式的应用过渡效果

  • 在 CSS 过渡和动画中自动应用 class
  • 可以配合使用第三方 CSS 动画库, 如 Animate.css
  • 在过渡钩子函数中使用 JavaScript 直接操作 DOM
  • 可以配合使用第三方 JavaScript 动画库, 如 Velocity.js

单元素/组件的过渡

Vue 提供了transition的封装组件, 可以给任何元素和组件添加进入/离开过渡

  • 条件渲染(使用v-if)
  • 条件展示(使用v-show)
  • 动态组件
  • 组件根节点
1
2
3
4
5
<div id="app">
<button @click="show = !show">toggle</button>
<hr />
<transition name="fade"> <p v-if="show">transition</p></transition>
</div>
1
2
3
4
5
6
7
8
new Vue({
el: 'app',
data() {
return {
show: false,
}
},
})
1
2
3
4
5
6
7
8
.fade-enter-active,
.fade-levce-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}

当插入或删除包含在 transition组件中的元素时, Vue 将会做以下处理
1.自动嗅探目标元素是否应用了 CSS 过渡效果或动画, 如果是, 在恰当的时机添加/删除 CSS 类名
2.如果过渡组件提供了 JavaScript 钩子函数, 这些钩子函数将在恰当的时机被调用
3.如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画, DOM 操作(插入/删除)在下一帧中立即执行

过渡的类名

在进入/离开的过渡中, 会有 6 个 class 切换

  1. v-enter: 定义进入过渡的开始状态, 在元素被插入之前生效, 在元素被插入之后的下一帧移除
  2. v-enter-active: 定义进入过渡生效时的状态, 在整个进入过渡的阶段中应用, 在元素被插入之前生效, 在过渡/动画完成之后移除. 这个类可以用来定义进入过渡的过渡时间, 延迟和曲线函数
  3. v-enter-to: 定义进入过渡的结束状态, 在元素被插入之后下一帧生效(与此同时v-enter被移除), 在过渡/动画完成之后移除
  4. v-leave: 定义离开过渡的开始状态, 在离开过渡被触发时立刻生效, 下一帧被移除
  5. v-leave-active: 定义离开过渡生效时的状态. 在整个离开过渡的阶段中应用, 在离开过渡被触发时立刻生效, 在过渡/动画完成之后移除, 这个类可以备用来定义离开过渡的过程时间, 延迟和曲线函数
  6. v-leave-to: 定义离开过渡的结束状态, 在离开过渡被触发之后下一帧生效(与此同时v-leave被删除), 在过渡/动画完成之后移除

transition

对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 ,则 v- 是这些类名的默认前缀。如果你使用了 ,那么 v-enter 会替换为 my-transition-enter。

v-enter-active 和 v-leave-active 可以控制进入/离开过渡的不同的缓和曲线,在下面章节会有个示例说明。

CSS 过渡

常用的都是使用 CSS 过渡

1
2
3
4
5
6
7
8
<div id="example-1">
<button @click="show = !show">
Toggle render
</button>
<transition name="slide-fade">
<p v-if="show">hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#example-1',
data: {
show: true,
},
})
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.slide-fade-enter-active {
transition: all 0.3s ease;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}

CSS 动画

CSS 动画用法与 CSS 过渡, 区别是在动画中 v-enter 类名在节点插入 DOM 后不会立即删除, 而是在 animationend事件触发时删除

1
2
3
4
5
6
7
8
9
10
<div id="example-2">
<button @click="show = !show">Toggle show</button>
<transition name="bounce">
<p v-if="show">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis
enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi
tristique senectus et netus.
</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#example-2',
data: {
show: true,
},
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}

自定义过渡的类名

我们可以通过以下 attribute 来自定义过渡类名:

  • enter-class
  • enter-active-class
  • enter-to-class
  • leave-class
  • leave-active-class
  • leave-to-class

他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css 结合使用十分有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<link
href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1"
rel="stylesheet"
type="text/css"
/>

<div id="example-3">
<button @click="show = !show">
Toggle render
</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#example-3',
data: {
show: true,
},
})

同时使用过渡和动画

Vue 为了知道过渡的完成,必须设置相应的事件监听器。它可以是 transitionendanimationend,这取决于给元素应用的 CSS 规则。如果你使用其中任何一种,Vue 能自动识别类型并设置监听。

但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如 animation 很快的被触发并完成了,而 transition 效果还没结束。在这种情况中,你就需要使用 type attribute 并设置 animation 或 transition 来明确声明你需要 Vue 监听的类型。

显性的过渡持续时间

在很多情况下,Vue 可以自动得出过渡效果的完成时机。默认情况下,Vue 会等待其在过渡效果的根元素的第一个 transitionend 或 animationend 事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果。

在这种情况下你可以用 <transition> 组件上的 duration 属性定制一个显性的过渡持续时间 (以毫秒计):

1
<transition :duration="1000">...</transition>

也可以定制进入和移出的持续时间:

1
<transition :duration="{ enter: 500, leave: 800 }">...</transition>

JavaScript 钩子

可以在属性中声明 JavaScript 钩子

1
2
3
4
5
6
7
8
9
10
11
12
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:enter-cancelled="enterCancelled"
v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
>
<!-- ... -->
</transition>
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
// ...
methods: {
// --------
// 进入中
// --------

beforeEnter: function (el) {
// ...
},
// 当与 CSS 结合使用时
// 回调函数 done 是可选的
enter: function (el, done) {
// ...
done()
},
afterEnter: function (el) {
// ...
},
enterCancelled: function (el) {
// ...
},

// --------
// 离开时
// --------

beforeLeave: function (el) {
// ...
},
// 当与 CSS 结合使用时
// 回调函数 done 是可选的
leave: function (el, done) {
// ...
done()
},
afterLeave: function (el) {
// ...
},
// leaveCancelled 只用于 v-show 中
leaveCancelled: function (el) {
// ...
}
}

这些钩子函数可以结合 CSS transitions/animations 使用,也可以单独使用。

当只用 JavaScript 过渡的时候,在 enter 和 leave 中必须使用 done 进行回调。否则,它们将被同步调用,过渡会立即完成。
推荐对于仅使用 JavaScript 过渡的元素添加 v-bind:css=”false”,Vue 会跳过 CSS 的检测。这也可以避免过渡过程中 CSS 的影响。

初始渲染的过渡

可以通过appearattribute 设置节点在初始渲染的过渡

1
2
3
<transition appear>
<!-- ... -->
</transition>

这里默认和进入/离开过渡一样, 同样也可以自定义 CSS 类名

1
2
3
4
5
6
7
8
9
<transition
appear
appear-class="custom-appear-class"
appear-to-class="custom-appear-to-class"
(2.1.8+)
appear-active-class="custom-appear-active-class"
>
<!-- ... -->
</transition>

自定义 JavaScript 钩子

1
2
3
4
5
6
7
8
9
<transition
appear
v-on:before-appear="customBeforeAppearHook"
v-on:appear="customAppearHook"
v-on:after-appear="customAfterAppearHook"
v-on:appear-cancelled="customAppearCancelledHook"
>
<!-- ... -->
</transition>

多个元素的过渡

我们之后讨论多个组件的过渡,对于原生标签可以使用 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 attribute 设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 <transition> 组件中的多个元素设置 key 是一个更好的实践。

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

过渡模式

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

多个组件的过渡

多个组件的过渡只需要使用动态组件

1
2
3
<transition name="component-fade" mode="out-in">
<component v-bind:is="view"></component>
</transition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>',
},
},
})
1
2
3
4
5
6
7
8
.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;
}

列表过渡

使用 <transition-group> 组件可以同时渲染整个列表

  • 不同于 ,它会以一个真实元素呈现:默认为一个 。你也可以通过 tag attribute 更换为其他元素。
  • 过渡模式不可用,因为我们不再相互切换特有的元素。
  • 内部元素 总是需要 提供唯一的 key 属性值。
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

列表的排序过渡

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

1
2
3
4
5
6
7
8
9
10
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

<div id="flip-list-demo" class="demo">
<button v-on:click="shuffle">Shuffle</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" v-bind:key="item">
{{ item }}
</li>
</transition-group>
</div>
1
2
3
4
5
6
7
8
9
10
11
new Vue({
el: '#flip-list-demo',
data: {
items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
},
methods: {
shuffle: function () {
this.items = _.shuffle(this.items)
},
},
})
1
2
3
.flip-list-move {
transition: transform 1s;
}

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

访问元素&组件

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。

访问根实例

这每个 new Vue 实例的子组件中, 其根实例可以通过$root属性进行访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Vue 根实例
new Vue({
data: {
foo: 1
},
computed: {
bar: function() {
/* ... */
}
},
methods: {
baz: function() {
/* ... */
}
}
})

所有的子组件都可以将这个实例作为一个 store 来访问或使用

1
2
3
4
5
6
7
8
9
10
11
// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()

访问父级组件实例

$root 类似, $parent 属性可以用来从一个子组件访问父组件的实例.

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>JS Bin</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app">
{{ msg }}
<hr />
<my-com />
</div>
<script>
Vue.component('my-com', {
template: '<h1>M</h1>',
mounted() {
// 访问父级
console.log(this.$parent.msg) // G
this.$parent.foo() // foo
}
})
var vm = new Vue({
el: '#app',
data() {
return {
msg: 'G'
}
},
methods: {
foo() {
console.log('foo')
}
}
})
</script>
</body>
</html>

访问子组件实例或子元素

有时候, 需要直接访问子组件, 可以通过 ref 这个 attribute 为子组件赋予一个 ID 引用

1
2
<!-- 子组件 -->
<my-component ref="my-com"></my-component>

可以在定义了这个 ref 的组件里使用

1
this.$refs['my-com'].msg

获取指定 DOM 元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="hello">
<hr />
<button @click="click">click</button>
<hr />
<input ref="input" />
</div>
</template>

<script>
export default {
methods: {
click() {
this.$refs['input'].focus()
}
}
}
</script>

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs。

依赖注入

我们可以虽然可以通过 $parent$refs、属性、事件等方式获取获取父组件或子组件的实例或属性, 但是无法扩展到更深层次级的嵌套组件上.
Vue 给我们提供了两个新的实例选项 provideinject, 选项允许我们指定我们想要提供给后代组件的数据/方法

1
2
3
4
5
6
7
8
9
10
11
// 根组件
provide: function() {
return {
getMap: this.getMap
}
}

// ... 第一层嵌套组件

// 第二层嵌套组件
inject: ["getMap"]

程序化的事件侦听器

除了 $emit可以被v-on侦听, Vue 同时还在事件接口中提供了其他方法:

  • 通过$on(eventName, eventHandler) 侦听一个事件
  • 通过$once(eventName, eventHandle) 一次性侦听一个事件
  • 通过$off(eventName, ebentHandle) 停止侦听一个事件

循环引用

递归组件

组件是可以在他们自己的模板中调用自身的, 不过它们只能通过 name 选项来做这件事

1
name: 'unique-name-of-my-component'

当你使用 Vue.component 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项。

1
2
3
Vue.component('unique-name-of-my-component', {
// ...
})

稍有不慎, 递归组件就可能导致无限循环

1
2
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 false 的 v-if)。

组件之间的循环引用

假设你需要构建一个文件目录树,像访达或资源管理器那样的。你可能有一个 <tree-folder> 组件,模板是这样的:

1
2
3
4
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children" />
</p>

还有一个<tree-folder-contents>组件

1
2
3
4
5
6
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child" />
<span v-else>{{ child.name }}</span>
</li>
</ul>

当你仔细观察的时候,你会发现这些组件在渲染树中互为对方的后代和祖先——一个悖论!当通过 Vue.component 全局注册组件的时候,这个悖论会被自动解开。如果你是这样做的,那么你可以跳过这里。
然而,如果你使用一个模块系统依赖/导入组件,例如通过 webpack 或 Browserify,你会遇到一个错误:

1
Failed to mount component: template or render function not defined.

为了解释这里发生了什么,我们先把两个组件称为 A 和 B。模块系统发现它需要 A,但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点,在那里“A 反正是需要 B 的,但是我们不需要先解析 B。”
在我们的例子中,把 <tree-folder> 组件设为了那个点。我们知道那个产生悖论的子组件是 <tree-folder-contents> 组件,所以我们会等到生命周期钩子 beforeCreate 时去注册它:

1
2
3
beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者,在本地注册组件的时候,你可以使用 webpack 的异步 import:

1
2
3
components: {
TreeFolderContents: () => import('./tree-folder-contents.vue')
}

模板定义的替代品

内联模板

inline-template 这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。

1
2
3
4
5
6
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>

内联模板需要定义在 Vue 所属的 DOM 元素内。
不过,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

X-Template

另一个定义模板的方式是在一个 <script> 元素中, 并为其带上 ext/x-template 的类型, 然后通过一个 id 将模板引用过去

1
2
3
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
1
2
3
Vue.component('hello-world', {
template: '#hello-world-template'
})

x-template 需要定义在 Vue 所属的 DOM 元素外。
这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

控制更新

感谢 Vue 的响应式系统,它始终知道何时进行更新 (如果你用对了的话)。不过还是有一些边界情况,你想要强制更新,尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新。

强制更新

你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。

然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事。

通过 v-once 创建低开销的静态组件

渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once attribute 以确保这些内容只计算一次然后缓存起来,就像这样:

1
2
3
4
5
6
7
8
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})

在动态组件上使用 keep-alive

我们可以使用 is attribute 来切换不同的组件

1
<my-componet v-bind:is="currentTabComponent"></my-component>

在切换组件的时候, 我们有时候需要保持这些组件的状态, 避免反复冲渲染导致性能的问题.
为了解决这个问题, Vue 为我们提供了 <keep-alive> 元素将其动态组件包裹起来

1
2
3
4
<!-- 失活的组件将会被缓存 -->
<keep>
<component v-binf:is="currentTabComponent"></component>
</keep>

<keep-alive> 元素要求被切换的组件都有自己的名字.

异步组件

我们可以将应用分隔成一些小代码块, 在组件需要被用到的时候才从服务器加载一个模块.
Vue 允许以一个工厂函数的方式定义一个组件, 这个工厂函数会异步解析组件定义
Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数, 并把结果缓存起来供未来重渲染

1
2
3
4
5
6
7
// 异步组件-在1秒后渲染
Vue.component('my-component', function(resolve, reject) {
setTimieout(() => {
resolve({ template: '<h1>标题</h1>' })
// reject() 组件加载失败
}, 1000)
})

将异步组件和webpackcode-splitting功能一起配合使用

1
2
3
4
5
6
Vue.component('my-component', function(resolve, reject) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})

也可以在工厂函数中返回一个 Promise

1
2
3
4
5
Vue.component(
'async-webpack-example',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)

局部注册时, 也可以直接提供一个返回Promise的函数

1
2
3
4
5
6
new Vue({
// ...
components: {
'my-component': () => import('./my-async-components')
}
})

处理加载状态

异步组件工厂函数也可以返回一个如下格式的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个`Promise`对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延迟时间, 默认200毫秒
delay: 200,
// 如果提供了超时时间且组件加载也超时了
// 则使用加载失败时使用的组件 默认 `Infinity`
timeout: 3000
})

插槽

在 2.6.0 中,具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。

插槽内容

Vue 实现了一套内容分发的 API, 它将 <slot> 元素作为承载分发内容的出口
<slot></slot> 插槽内可以包含任何模板内容, 可以是文本、html、其他组件

如果没有使用<slot>元素, 该组件起始标签和结束标签之间的任何内容都不会渲染

1
2
3
4
5
6
7
8
9
10
11
<!-- 子组件 -->
<my-component>
<slot></slot>
</my-component>

<!-- 父组件 -->
<my-parent>
<my-component>
这里的内容会替换到子组件中slot元素的位置
</my-component>
</my-parent>

编译作用域

如果想在一个插槽中使用数据, 该插槽跟模板其他地方一样可以访问相同的实例属性(相同的作用域)

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

默认值

我们在使用<slot>元素时, 可以放置一个默认内容, 当父组件中没有传入此项时, 该内容会被渲染, 当父组件有内容传入时, 传入内容替换掉默认内容, 被渲染出现

1
2
3
4
5
6
7
8
9
10
11
<!-- 子组件 -->
<my-child>
<slot>默认内容</slot>
</my-child>

<!-- 父组件 -->
<my-parent>
<my-child>
传入内容会替换掉默认内容
</my-child>
</my-parent>

具名插槽

v-slot 只能添加在<template>

有时候, 我们需要用到多个插槽, Vue 为<slot>元素提供了一个特殊的 name 属性, 可以用这个属性为额外的插槽进行定义
一个不带 name 的<slot>出口会带有隐含的名字”default”

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
<!-- 父组件 -->
<template>
<div class="hello">
<my-button>
<template v-slot:default>默认 name 插槽</template>
<template v-slot:title>
<h3>H3</h3>
</template>
<template v-slot:main>
<main>content</main>
</template>
</my-button>
</div>
</template>

<script>
import button from './button'
export default {
name: 'HelloWorld',
components: { 'my-button': button }
}
</script>

<!-- 子组件 -->
<template>
<div>
<slot>默认名为 default 的插槽</slot>
<p>
<slot name="title">默认内容</slot>
</p>
<slot name="main"></slot>
</div>
</template>

作用域插槽

有时候, 我们需要让插槽内容可以访问子组件中才有的数据
为了能在父级的插槽内访问到子组件的数据, 我们可以将数据作为<slot>元素的一个属性绑定上去

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

绑定在<slot>元素上的 attribute 被称为插槽 prop, 我们可以在父级作用域中, 使用带值的 v-slot 来定义我们提供的插槽 prop 名字

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

独战默认插槽的缩写语法

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

当被提供的内容只有默认插槽时, 我们可以不使用<template>元素, 而是将v-slot直接用在组件上

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

解构插槽

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

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

这意味着v-slot的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式. 所以我们可以通过解构的方式来传入具体的插槽 prop

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

将插槽 prop 重命名

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

动态插槽名

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

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

具名插槽的缩写

如果要使用缩写, 必须始终以明确插槽名取代

v-onv-bind一样, v-slot也有缩写, 即把参数之前的所有内容(v-slot:)替换为字符#

1
2
3
4
5
6
7
8
9
<base-layout>
<template #header>
<h1>缩写的具名插槽</h1>
</template>

<template #default>
<h1>缩写的默认插槽</h1>
</template>
</base-layout>

其他示例

插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容
我们可以将每个 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>

事件名

事件名需要完全匹配监听这个事件所用的名称

推荐使用短横线分隔法命名

1
this.$emit('myEvent')

监听这个事件

1
2
// 无效的做法
<my-component v-on:my-event="handleEmit"></my-component>

自定义组件的 v-modle

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input的事件.
单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的
model 选项可以用来避免这样的冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})

将原生事件绑定到组件

如果想要在一个组件的根元素上直接监听一个原生事件, 可以使用 v-on.native 修饰符

1
<base-inpur v-on.native="onFocus"></base-inpur>

很多时候, 自定义组件中根元素并不是我们真正想要监听的元素, 如下:
这时候, 父级的 .native 监听器将静默失败, 他不会产生任何报错, 但是 onFocus 处理函数不会如预期被调用.

1
2
3
4
5
6
7
8
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
/>
</label>

为了解决这个问题, Vue 提供了一个 $listeners 属性, 他是一个对象, 里面包含了作用在这个组件上的所有监听器

1
2
3
4
{
focus: function(event) {},
input: function(event) {}
}

有了这个 $listeners 属性,你就可以配合 v-on=”$listeners” 将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 <input> 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的:

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
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function() {
var vm = this
// `Object.assign` 将所有的对象合并为一个新对象
return Object.assign(
{},
// 我们从父级添加所有的监听器
this.$listeners,
// 然后我们添加自定义监听器,
// 或覆写一些监听器的行为
{
// 这里确保组件配合 `v-model` 的工作
input: function(event) {
vm.$emit('input', event.target.value)
}
}
)
}
},
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on="inputListeners"
>
</label>
`
})

现在 <base-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input> 元素一样使用了:所有跟它相同的 attribute 和监听器的都可以工作。

.sync 修饰符

有些特殊情况下, 我们可能需要对一个 prop 进行”双向绑定”, 这样很容易带来维护上的问题

1
this.$emit('update:title', newTile)

然后父组件可以监听那个事件并根据需要更新一个本地的数据属性

1
2
3
4
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>

Vue 为这种模式提供了一个缩写, 即 .sync 修饰符

1
<text-document v-bind:title.sync="doc.title"></text-document>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model。

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑

Prop 的大小写

在 HTML 元素的属性中对于大小写是不敏感的, 所以浏览器会将所有大写解释为小写字符, 意味着使用 DOM 模板时, 驼峰命名的 prop 名需要使用等价的短横线分隔命名

1
2
3
4
5
Vue.component('componentsName', {
// 驼峰命名
props: ['docTitle'],
template: '<h1>{{ docTitle }}</h1>'
})
1
2
<!-- 在 html 中是短横线分隔的 -->
<component-name doc-title="hello"></component-name>

Prop 类型

props 可以是个数组

1
props: ['title', 'name', 'content', 'gender']

很多时候我们都希望每个 prop 都有指定的值类型, 我们可以用对象的形式列出 prop, 如果传入的 prop 类型与定义不符, Vue 会在控制台进行提示

1
2
3
4
5
6
props: {
title: String,
name: String,
content: String,
gender: Boolean
}

传递静态或动态 Prop

我们可以将任意数据类型的值传递给 prop

我们可以给 prop 传入一个静态值

1
<my-component title="Ginger"></my-component>

我们也可以通过v-bind对 prop 进行动态赋值

1
2
3
4
const info = {
title: 'Ginger'
}
<my-component v-bind:title="info.title"></my-component>

单向数据流

所有的 prop 都使得父子组件的 prop 之间形成了一个单向下行绑定
父级的 prop 的更新会向下流动到子组件中, 无法反过来
因为每次父组件发生更新时, 子组件中所有 prop 都将会刷新为最新的值, 所以不应该在一个子组件内改变 prop

  • 需要将 prop 作为子组件内部数据来使用
1
2
3
4
5
6
props: ['initialCounter'],
data: function() {
return {
counter: this.initialCounter
}
}
  • 需要将传入的 prop 值进行转换
1
2
3
4
5
6
7
props:['size'],
// 使用一个计算属性来处理
computed: {
normalizedSize: function() {
return this.size.trim().toLowerCase()
}
}

prop 验证

我们可以为组件的 prop 指定验证要求, 如果不满足要求, Vue 会在控制台发出警告
为了定制 prop 的验证方式, 可以为 props 中的值提供一个验证需求的对象, 而不是一个字符串数组

验证失败 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
Vue.component('my-component', {
props: {
propA: Number, // 基础类型验证
propB: [String, Number], // 可以是多个类型
propC: {
// 必须传入的字符串
type: String,
require: true
},
propD: {
// 默认100的数字
type: Number,
default: 100
},
propE: {
// 带有默认的对象
type: Object,
default: function() {
return { name: 'Ginger' }
}
},
propF: {
// 自定义验证函数
validator: function(value) {
return ['success', 'waring', 'danger'].indexOf(value) !== -1
}
}
}
})

类型检测

type 可以是下列原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

另外, type 也可以是一个自定义的构造函数, 并且通过 instanceof 来进行检查确认

1
2
3
4
5
6
7
8
9
10
11
function Person(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}

// 使用
Vue.component('my-component', {
props: {
author: Person
}
})

非 Prop 的 Attribute

有些 attribute 通过自定义属性传向子组件, 但是在子组件内部并没有通过 props 接受, 这些 attribute 会被添加到这个组件的根元素上

替换/合并已有的 attribute

对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入 type="text 就会替换掉 type="date 并把它破坏!庆幸的是,classstyle attribute 会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:form-control date-picker-theme-dark

禁用 attribute 继承

如果不希望组件的根元素继承 attribute, 可以在组件的选项中设置 inheritAttrs: false

1
2
3
4
Vue.component('my-component', {
inheritAttrs: false,
...
})

$attrs 属性包含了传递给一个组件的 attribute 名和值

利用 inheritAttrs: false$attrs,就可以手动决定这些 attribute 会被赋予哪个元素

inheritAttrs: false 选项不会影响 style 和 class 的绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
`
})

组件名

在注册一个组件的时候, 我们始终需要给它一个名字. 我们在使用Vue.component注册组件时, 第一个参数就是组件名

Vue 推荐使用全小写字母且必须包含一个连字符

组件名大小写

定义组件名的方式有两种:
使用 kebab-case
使用组件: <my-component-name>

1
Vue.component('my-component-name', {})

使用 OascalCase
使用组件

  • <my-component-name>
  • <MyComponentName>
1
Vue.component('MyComponentName', {})

全局注册

当我们使用 Vue.component()来创建组件时, 这个组件时全局的, 我们可以在任何创建的 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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>JS Bin</title>
</head>
<body>
<div id="app">
<my-component></my-component>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<script>
Vue.component('my-component', {
data() {
return {
text: 'Ginger'
}
},
template: '<div>{{ text }}</div>'
})
var vm = new Vue({ el: '#app' })
</script>
</body>
</html>

局部注册

全局注册的组件即使在我们不使用的时候仍然会被包含在最终的构建结果中, 会造成用户下载的增加
我们可以通过一个普通 JavaScript 对象来定义组件, 然后在需要使用的 vue 实例中进行注册
对于 components 对象中的每个属性来说, 其属性名就是自定义元素的名字, 其属性值就是这个组件的选项对象

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>JS Bin</title>
</head>
<body>
<div id="app">
<app-hellp />
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<script>
var hello = {
data: function() {
return {
msg: 'Hi'
}
},
template: '<h1>{{msg}}</h1>'
}
var vm = new Vue({
el: '#app',
components: {
'app-hellp': hello
}
})
</script>
</body>
</html>

在模块系统中局部注册

我们也可以通过 require / import 使用一个模块系统.

在模块系统中局部注册

推荐创建一个 components 目录, 将每个组件防止在其各自的文件中

在模块系统中使用组件需要在局部注册之前, 我们需要导入每个想使用的组件

1
2
3
4
5
6
7
8
9
10
11
// 在使用前 先引入要使用的组件
import Home from './components/Home'
import Mine from './components/Mine'

export default {
components: {
Home,
Mine
}
...
}

基础组件的自动化全局注册

当我们使用了 webpack 构建应用时, 可以使用 require.context 只全局注册通用的基础组件

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
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
// 获取组件配置
const componentConfig = requireComponent(fileName)

// 获取组件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 获取和目录深度无关的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)

// 全局注册组件
Vue.component(
componentName,
// 如果这个组件选项是通过 `export default` 导出的,
// 那么就会优先使用 `.default`,
// 否则回退到使用模块的根。
componentConfig.default || componentConfig
)
})

全局注册必须在根 Vue 实例创建之前注册

基本示例

组件是可复用的 Vue 实例, 且带有一个名字, 我们可以通过 new Vue 创建的 Vue 跟实例中, 把这个组件当做自定义元素来使用
组件与 new Vue 接受相同的选项

组件的 data 必须是一个函数, 它返回一个对象的独立的拷贝
组件注册可分为 全局注册局部注册

1
2
3
4
5
6
7
8
Vue.component('hello-world', {
data() {
return {
msg: 'Hi! Vue',
}
},
template: '<div> {{ msg }} </div>',
})

组件的复用

组件可以进行任意次数的复用, 每个组件都会各自维护它的实例

1
2
3
4
5
<div id="app">
<hellp-world></hellp-world>
<hellp-world></hellp-world>
<hellp-world></hellp-world>
</div>

组件的组织

一个应用会以一颗嵌套的组件树的形式来组织:
component

通过 Prop 向子组件传递数据

Prop 是可以在组件上注册的一些自定义 attribute, 当一个值传递给一个 prop attribute 的时候, 他就变成了那个组件实例的一个属性.
我们可以用一个 props 选项将其包含在该组件可接受的 prop 列表中

1
2
3
4
Vue.component('hello-world', {
props: ['title'],
template: '<h3> {{ title }}</h3>',
})

单个根元素

每个组件必须只有一个根元素

当构建一个子组件的时候, 模板内最终包含的内容远不止一个标题, Vue 要求每个组件必须只有一个根元素, 可以将模板的内容包裹在一个父元素内.

1
2
3
4
5
6
7
8
9
10
// parent 父组件
<div class="parent">
<child />
</div>

// child 子组件
<div class="child">
<h3>标题</h3>
<p>内容</p>
</div>

监听子组件事件

有时候, 我们需要在子组件内部传递一些内容到父元素
Vue 实例提供了一个自定义事件的系统来解决这个问题, 父组件可以像处理 native DOM 事件一样通过 v-on 监听子组件实例的任意事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 父组件 接受一个由子元素提交的自定义方法
<div class="parent">
<child v-on:myEmit="handleEmit" />
</div>

// 子组件 传递一个自定义事件
<child v-on:click="$emit('myEmit')">
子组件
</child>

<hr />

// 父组件 接受一个由子元素提交的自定义方法
<div class="parent">
<child v-on:myEmit="handleEmit" />
</div>

// 子组件 自定义方法可以通过第二个参数进行传递
<child v-on:click="$emit('myEmit', true)">
子组件
</child>

在组件上使用 v-model

自定义事件也可以用于创建支持 v-model 的自定义输入组件

1
<my-input v-model="searchText" />

等价于

1
<my-input v-bind:value="searchText" v-on:input="searchText = $event" />

为了让它正常工作, 这个组件内的 <input> 必须:

  • 将其 value attribute 绑定到一个名叫 value 的 prop 上
  • 在其 input 事件被触发时, 将新的值通过自定义的 input 事件抛出

通过插槽分发内容

和 HTML 元素一样, 我们经常需要向一个组件传递内容

1
2
3
4
5
6
7
8
9
<!-- 父组件使用子组件 -->
<alert-box>
<span>插槽插入内容</span>
</alert-box>
<!-- 子组件 -->
<alert-box>
<!-- 插槽插入内容会替换下面的slot标签 -->
<slot></slot>
</alert-box>

动态组件

有的时候, 在不同的组件之间进行动态切换时非常有用的
我们可以通过 Vue 的<component>元素加一个特殊的isattribute 来实现
currentTabComponent 可以包括

  • 已注册组件的名字
  • 一个组件的选项对象
1
2
// 组件会在 currentTabComponent 改变时改变
<component v-bind:is='currentTabComponent'></component>

基础用法

你可以用 v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定. 它会根据控件类型自动选取正确的方法来更新元素. 但v-model的本质只是语法糖

v-model 会忽略所有表单元素的 vaulecheckedselect的初始值, 而使用 Vue 实例中数据作为数据来源, 所以我们需要在 data 中声明初始值

v-model 在内部为不同的表单元素使用不同的属性并抛出不同的事件:

  • text 和 textarea 元素使用 value 属性和 input 事件
  • checkbox 和 radio 使用 checked 属性和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件

文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="hello">
<input type="text" v-model="msg" />
<textarea v-model="msg"></textarea>
<p>{{ msg }}</p>
</div>
</template>

<script>
export default {
data() {
return {
msg: 'Hi'
}
}
}
</script>

复选框

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
<template>
<div class="hello">
<!-- 单个复选框 -->
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox"></label>
<p>{{ checked }}</p>
<hr />
<!-- 多个复选框 -->
<input type="checkbox" id="Jack" value="Jack" v-model="checkes" />
<input type="checkbox" value="Ginger" v-model="checkes" />
<input type="checkbox" value="Som" v-model="checkes" />
<p>{{ checkes }}</p>
</div>
</template>

<script>
export default {
data() {
return {
checked: false,
checkes: []
}
}
}
</script>

单选框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="hello">
<label for="y">
Yes
<input type="radio" id="y" value="y" v-model="res" />
</label>
<label for="n">
No
<input type="radio" id="n" value="n" v-model="res" />
</label>
<p>{{ res }}</p>
</div>
</template>

<script>
export default {
data() {
return {
res: ''
}
}
}
</script>

选择框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div class="hello">
<select v-model="selectd">
<option disabled value>点击选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>{{ selectd }}</p>
</div>
</template>

<script>
export default {
data() {
return {
selectd: ''
}
}
}
</script>

修饰符

.lazy 修饰符

v-model 会在每次 input 事件触发后将输入框的值与数据进行同步, 可以通过添加 .lazy 修饰符, 从而转变为使用 change 事件进行同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="hello">
<input v-model.lazy="msg" />
<br />
<span>change update: {{ msg }}</span>
</div>
</template>

<script>
export default {
data() {
return {
msg: ''
}
}
}
</script>

.number 修饰符

如果想将用户输入值转为数值类型, 可以给 v-model 添加 number 修饰符

1
2
<!-- string => number -->
<input v-model.number="age" type="number" />

.trim

如果要自动过滤用户输入的收尾空白字符, 可以给 v-model 添加 trim 修饰符

1
<input v-model.trim="msg" />

在组件上使用 v-model

在原生 HTML 的输入元素不能满足需求时, Vue 允许创建自定义行为的输入组件, 并且可以绑定 v-model 一起使用