Vue组件


title: Vue组件

date: 2019-09-09 11:23:52

tags:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • Vue.js # 一级分类
  • Vue.js基础 # 二级分类

作用

提高代码复用性

组件使用方法

全局注册

全局注册时,Vue.component需要在new Vue实例之前注册.否则报错.

1
2
3
4
5
6
7
8
9
Vue.component('button-counter',{
data: function(){
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
})

优点:所有的 vue 实例都可以用

缺点:权限太大,容错率降低

局部注册

1
2
3
4
5
6
7
8
var app = new Vue({
el: "#app",
components: {
"my-component": {
template: "<div>我是组件的内容</div>",
},
},
});

注意局部注册的组件在其子组件相互之间是不可用的。

如果你希望 ComponentA 在 ComponentB 中可用,则你需要这样写:

1
2
3
4
5
6
7
8
9
10
var ComponentA = {
/* ... */
};

var ComponentB = {
components: {
"component-a": ComponentA,
},
// ...
};

或者采用 import 方法:

1
2
3
4
5
6
7
8
import ComponentA from "./ComponentA.vue";

export default {
components: {
ComponentA,
},
// ...
};

组件的复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>

<script>
Vue.component('button-counter',{
data: function(){
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
})
</script>

问题: 点击按钮时,三个按键的数字都会变化吗?为什么?

注意当点击按钮时,每个组件都会各自独立维护它的count。因为你每用一次组件,就会有一个它的新实例被创建。

特殊情况

vue 组件的模板在某些情况下会受到 html 标签的限制,比如 <table> 中只能还 有 <tr> , <td>这些元素,所以直接在 table 中使用组件是无效的,此时可以使用is属性来挂载组件

1
2
3
<table>
<tbody is="my-component"></tbody>
</table>

组件使用技巧

  1. 必须使用小写字母加 ­ 进行命名child,my­component命名组件
  2. template 中的内容必须被一个 DOM 元素包括,也可以嵌套
  3. 在组件的定义中,除了 template 之外还可以使用其他选项,比如data,computed,methods
  4. 一个组件的 data 选项必须是一个函数,否则就会出现点一个其他按钮也跟着变化的情况

组件中 data 什么时候可以用对象

因为组件内 data 会复用。一个组件修改,就都会改

解决办法:

new Vue(),生成一个根实例。该组件不会复用,也就不会共享。

组件通信

通过 prop 父传子通信

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
<div id="blog-post-demo">
<h5>我是父组件</h5>
<child-post
msg="我是来自父组件的内容"
v-for="post in posts"
v-bind:key="post.id"
v-bind:title="post.title"
>
</child-post>
</div>

<script>
Vue.component('child-post',{
props: ['title','msg'],
template: '<h3>{{ title }} + {{msg}}</h3>'
})

new Vue({
el: "#blog-post-demo",
data: {
posts: [
{id: 1, title: 'aaa' },
{id: 2, title: 'bbb' },
{id: 3, title: 'ccc' }
]
}
}
)

</script>

一个组件默认可以拥有任意数量的prop,任何值都可以传递给任何prop。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问data中的值一样。

如上所示,你会发现我们可以使用v-bind来动态传递prop。这在你一开始不清楚要渲染的具体内容,比如从一个 API 获取博文列表的时候,是非常有用的。

总结

  1. 在组件中使用props来从父亲组件接收参数,注意,在props中定义的属性,都可以在组件中直接使用
  2. props 来自父级,而组件中 data return 的数据就是组件自己的数据,两种情况作用域就是 组件本身,可以在 template,computed,methods 中直接使用
  3. props 的值有两种,一种是字符串数组,一种是对象,本节先只讲数组
  4. 可以使用 v-­bind 动态绑定父组件来的内容

单向数据流

  • 解释:  通过props传递数据是单向的了,也就是父组件数据变化时会传递给子组件,但是反过来不行。
  • 目的: 是尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态。
  • 应用场景: 业务中会经常遇到两种需要改变prop的情况

一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。这种情况可以在组件 data 内再声明一个数据,引用父组件的prop

步骤一:注册组件

步骤二:将父组件的数据传递进来,并在子组件中用 props 接收

步骤三:将传递进来的数据通过初始值保存起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
<my-comp init-count="666"></my-comp>
</div>
<script>
var app = new Vue({
el: '#app',
components: {
'my-comp': {
props: ['init-count'],
template: '<div>{{init-count}}</div>',
data: function() {
return {
//初始值count.props中的值通过this.XXX获取
count: this.initCount
}
}
}
}
})
</script>

另一种情况就是prop作为需要被转变的原始值传入。这种情况用计算属性就可以了

步骤一:注册组件

步骤二:将父组件的数据传递进来,并在子组件中用 props 接收

步骤三:将传递进来的数据通过计算属性进行重新计算

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
<div>
<input type="text" v-model="width">
<my-comp :width="width"></my-comp>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
width: ''
},
components: {
'my-comp': {
props: ['init-count', 'width'],
template: '<div :style="style">{{ init-count }}</div>',
computed: {
style: function(){
return {
width: this.width + 'px',
background: 'red'
}
}
}
}
}
})
</script>

驼峰命名与短横线命名

  1. 在 html 中,myMessagemymessage是一样的.因此在组件中的 html 中必须使用短横线命名.
  2. 在组件中,父组件给子组件传数据必须用短横线(属于 HTML 范围内),因为 html 不识别驼峰,在props中无所谓.在template中必须使用驼峰命名.
  3. 在组件中的data中,用this.XXX引用时,必须使用驼峰命名.

数据验证

验证的  type  类型可以是:

• String

• Number

• Boolean

• Object

• Array

• Function

在 props 中设定数据类型时,必须使用对象格式,即

1
2
3
4
5
6
7
8
9
10
11
12
props: {
//msg必须是数字
msg: Number,
//total既可以是数字又可以是字符串
total: [Number,String]
//ach是布尔值,默认是true,required是必传项.
ach: {
type: Boolean,
default: true,
required: true
}
}

组件通信

组件通信分为父子通信,兄弟通信,跨级通信.

子传父通信

使用v­-on除了监听 DOM 事件外,还可以用于组件之间的自定义事件.

JavaScript 的设计模式 一一观察者模式, dispatchEventaddEventListener这两个方法.Vue 组件也有与之类似的一套模式,

子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件.

  1. 自定义事件change
  2. 在子组件中用$emit触发change,即this.$emit('change',this.count).括号里前面是用户名,后面是传递参数.
  3. 在自定义事件中用一个参数来接受
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
<div id="app">
<p>您的余额为{{ total }}</p>
//父组件内用v-on监听change事件
<btn-component @change="handleTotal"></btn-component>
</div>

<script>
var app = new Vue({
el: "#app",
data: {
total: 0
},
methods: {
handleTotal: function(value){
this.total = value
}
},
components: {
'btn-component': {
template: '<div>\
<button @click="handleIncrease">+1</button>\
<button @click="handleReduce">-1</button>\
</div>',
data: function(){
return {
count: 0
}
},
methods: {
handleIncrease: function(){
this.count++;
this.$emit('change', this.count)
},
handleReduce: function(){
this.count--
this.$emit('change', this.count)
}
}
}
}

})

</script>

在组件中使用 v-model

$emit的代码实际上会触发一个 input 事件,input后的参数就是传递给v-model绑定的属性的值.

v-model其实是一个语法糖,其实绑定了两层操作:

  1. v-bind绑定一个 value 值
  2. v-on指令给当前元素绑定 input 事件
1
2
3
4
5
6
<input v-model="total">
//等价于
<input
v-bind:value="total"
v-on:input="total = $event.target.value"
>

要使用v-model,要做到:

  1. 接收一个 value 属性
  2. 在有新的 value 时触发 input 事件

修改上面的实例:

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
<div id="app">
<p>您的余额为{{ total }}</p>
<btn-component v-model="total"></btn-component>
//如果又添加一个相同的组件,点击也会变化,只不过显示的值属于各自的结果,不冲突
<btn-component v-model="total"></btn-component>
</div>
<script>
var app = new Vue({
el: "#app",
data: {
total: 0
},
components: {
'btn-component': {
template: `<div>
<button @click="handleincrease">+1</button>
<button @click="handlereduce">-1</button>
</div>`,
data: function(){
return {
count: 0
}
},
methods: {
handleincrease: function(){
this.count++
this.$emit('input',this.count)
},
handlereduce: function(){
this.count--
this.$emit('input',this.count)
}
}
}
}
})
</script>

V-model 简单实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<h5>我是父组件</h5>
<input type="text" v-model="parentmsg">
// 下面的是子组件,通过v-bind绑定msg到父组件中v-model所绑定的parentmsg,通过props传递到模板中渲染到页面
<child :msg="parentmsg"></child>
</div>

<script>
Vue.component('child',{
props: ['msg'],
template: '<h3>{{msg}}</h3>'
})

new Vue({
el: "#app",
data: {
parentmsg:''
}
}

)
</script>

非父子组件通信(兄弟通信)

方法: 使用 bus 中介

使用一个空的 Vue 实例作为中央事件总线(bus):

var bus = new Vue()

触发组件 A 中的事件

bus.$emit('id-selected',1)

在组件 B 创建的钩子中监听事件

bus.$on('id-selected',function(id){//....})

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
<div id="app">
<my-acomponent></my-acomponent>
<my-bcomponent></my-bcomponent>
</div>

<script>
Vue.component('my-acomponent',{
template: `<div><button @click="handle">点击我向B组件传数据</button></div>`,
data: function(){
return {
aaa: '我来自a组件'
}
},
methods: {
handle: function(){
this.$root.bus.$emit('lala', this.aaa)
}
}
})

Vue.component('my-bcomponent',{
template: `<div>我是B组件</div>`,
created: function(){
//A组件在实例创建的时候就监听事件--lala事件
this.$root.bus.$on('lala',function(value){
alert(value)
})
}
})

var app = new Vue({
el:"#app",
data: {
//bus中介
bus: new Vue()
}
})
</script>

父链和子链

this.$parent(从父组件里拿内容)

1
2
3
4
5
6
7
8
Vue.component("child-component", {
template: `<button @click="setFatherData">通过我修改父亲的数据</button>`,
methods: {
setFatherData: function () {
this.$parent.msg = "数据已修改";
},
},
});

this$refs(从子组件里拿内容 )

为子组件提供索引的方法,用特殊的属性 ref 为其增加一个索引

如果用$children会把所有子组件都拿到

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
<div id="app">
<my-acomponent ref="A"></my-acomponent>
<my-bcomponent ref="B"></my-bcomponent>
<button @click="getChildData">我是父组件,我要拿到子组件的数据</button>---{{ msg }}
</div>
<script>
var app = new Vue({
el: "#app",
data: {
bus: new Vue()
msg: '数据未拿到',
formchild: '还未拿到'
},
methods: {
getChildData: function(){
//用来拿子组件中的内容---$refs
this.fromChild = this.$refs.b.msg
}
}

})

Vue.component('my-acomponent',{
template: `<div></div>`,
data: function(){
return {
msg: '我来自a组件'
}
}
})
</script>

使用 slot 插槽分发内容

什么是插槽

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为内容分发.Vue.js  实现了一个内容分发 API,使用特殊的slot元素作为原始内容的插槽.

编译的作用域

父组件的作用域在父组件内,即<div id="app"></div>内部(便于理解).

子组件的作用域在子组件的template里.

插槽的用法

混合父组件的内容和子组件的模板

  1. 单个插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<my-component>
<p>父组件</p>
//虽然是父组件的作用域,但没有slot插槽,父组件的信息是无法显示
//插槽的作用是把父组件的内容插入到下面的子组件中,最后会显示出来
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `<div>
<slot>
如果父组件没有插入内容,那么我就作为默认出现
</slot>
</div>`
})
var app = new Vue({
el:"#app",
data: {

}
})
</script>
  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
<div id="app">
<my-component>
<h3 slot="header">我是标题</h3>
//给父组件的插槽命名,可以和子组件模板的slot一一对应
<p>父组件</p>
//这个不命名的自然就对应那个没有命名的slot
<p slot="footer">我是底部信息</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `<div>
<div class="header">
<slot name="header"></slot>
</div>
<div class="container">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>`
})
var app = new Vue({
el:"#app",
data: {

}
})
</script>
  1. 作用域插槽

作用域插槽是一种特殊的插槽,使用一个可复用的模板来替换已渲染的元素

  • 从子组件获取数据
  • <template>标签是不会渲染出来的,Vue 版本更新后,也可以写在其他标签上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="app">
<my-component>
<template slot="abc" slot-scope="prop">
//写一个临时变量prop,用临时变量拿子组件的信息
{{ prop.text }}
</template>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `<div>
<slot name="abc" text="我是子组件的内容">
</slot>
</div>`
})
var app = new Vue({
el:"#app",
data: {

}
})
</script>

访问slot

通过this.$slots.(name)

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
<div id="app">
<my-component>
<h3 slot="header">我是标题</h3>
<p>父组件</p>
<p slot="footer">我是底部信息</p>
</my-component>
</div>
<script>
Vue.component('my-component',{
template: `<div>
<div class="header">
<slot name="header"></slot>
</div>
<div class="container">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>`,
mounted: function(){
//访问插槽
var header = this.$slots.header
var text = header[0].elm.innerText
console.log(header)
//打印一个虚拟节点
console.log(text)
}
})
var app = new Vue({
el:"#app",
data: {

}
})
</script>

组件高级用法-动态组件

实现需求: 点击不同按钮切换不同页面

使用is动态绑定组件,调用方法切换不同页面

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
<div id="app">
<component :is="thisView"></component>
<button @click="handleView('A')">第一句</button>
<button @click="handleView('B')">第二句</button>
<button @click="handleView('C')">第三句</button>
<button @click="handleView('D')">第四句</button>
</div>
<script>
Vue.component('comA',{
template:`<div>鹅鹅鹅</div>`
})
Vue.component('comB',{
template:`<div>曲项向天歌</div>`
})
Vue.component('comC',{
template:`<div>白毛浮绿水</div>`
})
Vue.component('comD',{
template:`<div>红掌拨清波</div>`
})

var app = new Vue({
el: "#app",
data: {
thisView: 'comA'
},
methods: {
handleView: function(tag){
this.thisView = 'com' + tag
}
}
})
</script>

解析 DOM 模板时的注意事项

有些 HTML 元素,诸如<ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li><tr><option>,只能出现在其它某些特定的元素内部。

1
2
3
<table>
<blog-post-row></blog-post-row>
</table>

这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的is特性给了我们一个变通的办法:

1
2
3
<table>
<tr is="blog-post-row"></tr>
</table>

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在的:

  1. 字符串 (例如:template: ‘…’)
  2. 单文件组件 (.vue)
  3. <script type="text/x-template">

模块系统

在模块系统中局部注册

推荐创建一个 components 目录,并将每个组件放置在其各自的文件中。

然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的 ComponentB.jsComponentB.vue 文件中:

1
2
3
4
5
6
7
8
9
10
import ComponentA from "./ComponentA";
import ComponentC from "./ComponentC";

export default {
components: {
ComponentA,
ComponentC,
},
// ...
};

现在 ComponentA 和 ComponentC 都可以在 ComponentB 的模板中使用了

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

可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为基础组件,它们会在各个组件中被频繁的用到。

所以会导致很多组件里都会有一个包含基础组件的长列表:

1
2
3
4
5
6
7
8
9
10
11
import BaseButton from "./BaseButton.vue";
import BaseIcon from "./BaseIcon.vue";
import BaseInput from "./BaseInput.vue";

export default {
components: {
BaseButton,
BaseIcon,
BaseInput,
},
};

如果你使用了 webpack (或在内部使用了 webpack 的 Vue CLI 3+),那么就可以使用 require.context 只全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件 (比如 src/main.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
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 实例 (通过 new Vue) 创建之前发生.

Prop

Prop 的大小写

HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符.

当使用 dom 中的模板时,驼峰命名的 prop 需要使用其等价的短横线命名替换:

1
2
3
4
5
6
7
8
9
10
//js
Vue.component('blog-post', {
// 在 JavaScript 中是 驼峰 的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})

//html
<!-- 在 HTML 中是短横线的 -->
<blog-post post-title="hello!"></blog-post>

prop 的类型

字符串和对象

字符串:props: ['title', 'likes', 'isPublished', 'commentIds', 'author']

对象:

1
2
3
4
5
6
7
8
9
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // or any other constructor
}

传递静态或动态 Prop

传静态值:

1
<blog-post title="My journey with Vue"></blog-post>

动态赋值:

1
<blog-post v-bind:title="post.title"></blog-post>

布尔值:

1
2
3
4
5
6
7
8
9
<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>

数组

1
2
3
4
5
6
<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>

对象

1
2
3
4
5
6
7
8
9
10
11
<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
v-bind:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>

禁用特性继承

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

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

这尤其适合配合实例的$attrs 属性

1
2
3
4
{
required: true,
placeholder: 'Enter your username'
}
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>
`,
});
1
2
3
4
5
6
//html
<base-input
v-model="username"
required
placeholder="Enter your username"
></base-input>

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

自定义事件

事件名

不同于组件和 prop,事件名不会被用作一个 js 变量名或属性名,所以就没有理由使用驼峰命名了。并且v-on事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以推荐始终使用短横线命名.

自定义组件的 v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value 特性用于不同的目的。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-model 的时候:

1
<base-checkbox v-model="lovingVue"></base-checkbox>

这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox>触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的属性将会被更新。

注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。

将原生事件绑定到组件

使用v-on的修饰符.native直接监听一个组件根元素上的原生事件

1
<base-input v-on:focus.native="onFocus"></base-input>

但当上述 <base-input> 组件做了如下重构,那么根元素实际上是一个 <label> 元素:

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>

这时,父级的 .native 监听器将静默失败。它不会产生任何报错,但是 onFocus 处理函数不会如你预期地被调用。

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

1
2
3
4
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}

$listeners

有了这个 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>元素一样使用了:所有跟它相同的特性和监听器的都可以工作。

.sync 修饰符

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。

这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:

1
this.$emit("update:title", newTitle);

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

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

为了方便起见,我们为这种模式提供一个缩写,即 .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 配合使用:

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

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

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

动态组件和异步组件

动态组件 is

使用is特性切换不同的组件,如果想在标签的组件实例被在它们第一次被创建的时候缓存下来,避免重复渲染.可以使用<keep-alive> 元素将其动态组件包裹起来.

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

异步组件

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

异步组件的 3 种实现方式—工厂函数、Promise、高级函数

异步组件实现的本质是 2 次渲染,先渲染成注释节点,当组件加载成功后,在通过 forceRender 重新渲染

高级异步组件可以通过简单的配置实现 loading   resolve   reject   timeout  4 种状态.

例如:

1
2
3
4
5
6
7
8
Vue.component("async-example", function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: "<div>I am async!</div>",
});
}, 1000);
});

函数有 2 个参数,resolve 和 reject,它们是两个函数,由 javascript 引擎提供,不用自己定义。resolve 会在你从服务器得到组件定义的时候被调用。

如你所见,这个工厂函数会收到一个 resolve回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己.

一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:

1
2
3
4
5
6
7
8
Vue.component("async-webpack-example", function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(["./my-async-component"], resolve);
});
//这里使用的是webpack模块方法require(AMD版本),将其对应的文件拆分到一个单独的 bundle 中,
//此 bundle 会被异步加载,然后调用resolve回调函数

你也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,我们可以写成这样:

1
2
3
4
5
6
Vue.component(
"async-webpack-example",
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import("./my-async-component")
);
//这里使用的webpack模块方法import(),可以通过注释的方法定义新chunk的名称

处理加载状态

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

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,
});

当使用局部注册的时候,也可以直接提供一个返回 promise 的函数,比如在使用 vue 的路由懒加载时:

1
2
3
4
//懒加载方式,当路由被访问时才加载对应组件
const Login = () => import("@/components/Login");
const Home = (resolve) => require(["@/components/Home"], resolve);
const UserList = (resolve) => require(["@/components/user/list"], resolve);

这样只有当要访问路由时,才会加载指定路由下的组件.

处理边界情况

访问元素和组件

访问根实例

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

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
// Vue 根实例
new Vue({
data: {
foo: 1,
},
computed: {
bar: function () {
/* ... */
},
},
methods: {
baz: function () {
/* ... */
},
},
});

// 获取根组件的数据
this.$root.foo;

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

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

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

访问父组件实例

$root类似,$parent 属性可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

访问子组件实例

通过$ref

依赖注入

依赖注入用到了两个新的实例选项:provideinject

provide 选项允许我们指定我们想要提供给后代组件的数据/方法。在这个例子中,就是 <google-map> 内部的 getMap 方法

1
2
3
4
5
provide: function () {
return {
getMap: this.getMap
}
}

然后在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的属性:

1
inject: ["getMap"];

相比$parent来说,这个用法可以让我们在任意后代组件中访问 getMap,而不需要暴露整个 <google-map> 实例。这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和 props 一样。

实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  1. 祖先组件不需要知道哪些后代组件使用它提供的属性
  2. 后代组件不需要知道被注入的属性来自哪里

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。

程序化的事件侦听器

现在,你已经知道了 $emit 的用法,它可以被 v-on 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法。我们可以:

  1. 通过 $on(eventName, eventHandler) 侦听一个事件
  2. 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  3. 通过 $off(eventName, eventHandler) 停止侦听一个事件

让多个输入框元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}

循环引用

递归组件

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

1
name: "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> 组件设为了那个点。我们知道那个产生悖论的子组件是 <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)

当子组件中出现inline-template时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。

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> 元素中,并为其带上 text/x-template 的类型,然后通过一个 id 将模板引用过去。例如:

1
2
3
4
5
6
7
8
9
//html
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>;

//js
Vue.component("hello-world", {
template: "#hello-world-template",
});

x-template 需要定义在 Vue 所属的 DOM 元素外。

这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

控制更新

强制更新

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。

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

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

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

渲染包含大量静态内容的组件,你可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来;

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>
`,
});

注意事项:

不要过度使用这个模式。当需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。