一、注册组件
1、全局注册
注册组件的方式为:
Vue.component(tagName, options);
注意:组件的注册,要在实例化实例之前完成。即:
Vue.component('my-component', {
template: '<div>This is a component</div>'
});
new Vue({
el: '#app'
});
执行效果:
<div id="app">
<my-component></my-component>
</div>
<!-- 渲染为 -->
<div id="app">
<div>This is a component</div>
</div>
2、局部注册
除了全局注册一个组件外,我们还可以使用局部注册来注册一个组件,这种情况下,组件仅在另一个实例/组件的作用域中可用,使用方法为:
var COM = {
template: '<div>This is a local component</div>'
};
new Vue({
el: '#app',
components: {
'my-component': COM
}
})
3、模板解析
由于一些HTML元素限制了其自身能够包裹的内容(如<ul>
、<table>
、<select>
这些元素),因此,对于这些受限制的组件,不宜用以下的方式来使用组件:
<table>
<my-row></my-row>
</table>
而应该使用以下的方式:
<table>
<tr is="my-row"></tr>
</table>
不过,如果使用以下来源之一的字符串模板,就可以避开这些限制:
<script type="text/x-template">
- JavaScript内联模板字符串
.vue
组件
二、组件中的data
在Vue中,组件中使用的data
,应该是一个函数,而不应该是一个对象。即:
Vue.component('my-component', {
template: '<span>{{ message }}</span>',
data: {
message: 'Hello'
}
});
是会报错的,正确的使用方式应该为:
Vue.component('my-component', {
template: '<span>{{ message }}</span>',
data: function() {
return {
message: 'Hello'
}
}
});
这是为了让每个组件都拥有各自的内部状态
三、父子组件协作
Vue中,父子组件之间的协作,主要通过:props down,events up
来实现,即:
Parent
→ 传递props →Child
Parent
← 发送事件 ←Child
1、父组件给子组件传递数据(通过props
)
组件的实例的作用域是孤立的,因此,子组件内是没有办法直接引用到父组件数据的。因此,Vue提供了props
选项,来实现父组件数据到子组件的传递。当子组件需要用到父组件的数据时,子组件就在自己的option中的props
里声明期望得到的数据,然后由父组件传递给它。如:
Vue.component('say', {
props: ['message'], // 期望得到message这个数据
template: '<span>{{ message }}</span>'
});
new Vue({
el: '#app'
});
那么,父组件就可以通过给子组件添加一个props中对应的属性,来将数据传递给子组件了。即:
<div id="app">
<say message="Hello, world"></say>
</div>
如此一来,message
属性中的内容便传入了子组件,最终会渲染得到:
<div id="app">
<span>Hello, world</span>
</div>
除了使用字符串字面量来传入props的值,我们还可以使用动态的props,如:
<div id="app">
<input type="text" v-model="message" />
<say v-bind:message="message"></say>
</div>
注意: props中的属性,如果是camelCase形式的,那么在HTML中需要转化为kebab-case(因为HTML不区分大小写),即:props: ['myMessage']
,传入的时候应该使用形如<say my-message="Hello, world"></say>
此外,直接使用字面量形式传递的属性,其值类型为String
,这种情况下,会导致一定的问题,如<comp age="21"></comp>
,age的值类型为string,而非预期的number。所以,如果要解决这个问题的话,那么就应当使用v-bind
,即:<comp v-bind:age="21"></comp>
单向数据流
prop的传递是单向传递的,即:父组件属性变化时,会传递给子组件,但是子组件的数据变化时,不会传给父组件。如此设计,是为了避免子组件无意中改变了父组件的状态。
如果想修改props中的数据,正确的方式应该是:
1)定义一个局部变量,使用prop的值初始化,即:
props: ['message'],
data: function() {
return {
mymessage: this.message
}
}
2)定义一个计算属性,处理并返回
props: ['message'],
computed: {
mymessage: function() {
this.message.toUpperCase();
}
}
限制props
我们可以为props传入一些参数,来限制props的数据规则。此时,就不能再使用字符串数组指定props了,而是传入一个对象。如:
Vue.component('example', {
props: {
// 限制必须为基础类型,如果传入null,则任何类型都可以
A: Number,
// 限制可以为一组的基础类型
B: [Number, String],
// 既要限制为某个基础类型,也要求必传这个参数,且指定默认值
C: {
type: String,
required: true,
default: 'hello' // 默认值可以是字面量,也可以是一个工厂函数返回
},
// 数组/对象的默认值应该使用工厂函数返回,如:
D: {
type: Object,
default: function() {
return { message: 'Hello' }
}
},
// 自定义验证函数
E: {
validator: function(value) {
return value > 10
}
}
}
});
当props
验证失败的时候,如果当前使用的是开发版本,那么Vue会抛出一个警告。此外,props的校验,是在组件实例创建之前进行的
2、子组件给父组件传递消息
子组件给父组件传递数据,主要使用自定义事件
。Vue的自定义事件,主要是:
- 使用
$on(eventName)
来监听事件 - 使用
$emit(eventName)
来触发事件
$on
行为类似于addEventListener
,而$emit
行为类似于dispatchEvent
,但是它们并不是这两个函数的别名。事实上,Vue中的这两个函数,是分离自EventTarget API
的
例子:我们想实现一个父子组件,父组件有一个计数器,它的值为子组件的计数器值之和。那么我们可以使用Vue的事件机制来实现,即如:
<div id="app">
{{ parentCounter }}
<sub-counter v-on:subinc="addParentCounter"></sub-counter>
<sub-counter v-on:subinc="addParentCounter"></sub-counter>
</div>
Vue.component('sub-counter', {
template: '<button v-on:click="increment">{{counter}}</button>',
data() {
return {
counter: 0
}
},
methods: {
increment() {
this.counter++; // 自身+1
this.$emit('subinc'); // 发送+1事件给父组件
}
}
});
new Vue({
el: '#app',
data: {
parentCounter: 0
},
methods: {
addParentCounter() {
this.parentCounter++;
}
}
});
如果想要监听一个原生事件的话,可以使用.native
修饰v-on
,如:
<my-component v-on:click.native="doTheThing"></my-component>
3、自定义事件的表单输入组件
对于<input v-model="something">
,它相当于<input v-bind:value="something" v-on:input="something = $event.target.value">
。如果要在组件中使用v-model
,让它生效的话,组件需要满足:
- 接受一个
value
属性 - 在有新的value时触发
input
事件
示例:一个简单的货币输入的自定义控件
<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
template: '<span>$ <input ref="input" v-bind:value="value" v-on:input="updateValue($event.target.value)"></span>',
props: ['value'], // 父组件需要给子组件传递一个value
methods: {
updateValue: function(value) {
var formattedValue = value.trim().slice(0, value.indexOf('.')+3);
if(formattedValue !== value) {
this.$refs.input.value = formattedValue;
}
this.$emit('input', Number(formattedValue));
}
}
});
4、非prop属性
非prop
属性,是指可以直接传入子组件,但是不需要预先写在props
里的属性。
这种情况下,属性会被自动添加到组件模板中的根元素上,如:
<bs-date-input data-3d-date-picker="true"></bs-date-input>
其中bs-date-input
组件的模板为:
<div>Hello!!</div>
那么添加data-3d-date-picker="true"
后,将有:
<div data-3d-date-picker="true">Hello!!</div>
而如果data-3d-date-picker
是props属性,那么则会生成:
<div>Hello!!</div>
现在,我们会遇到有一种情况:在父组件使用子组件的时候,给子组件传了一个非prop属性,但是子组件里有同名的属性,如:
父组件中:
<my-input class="special"></my-input>
子组件中:
Vue.component('my-input', {
template: `<input class="form-control">`,
// ...
})
这种情况下,Vue会如何处理呢?情况是这样子的:
1)对于class
和type
这种属性,vue会做合并处理,所以上例子中最后会生成:
<input class="form-control special">
2)其他的属性,则会做替换处理(父组件中传入的值替换子组件模板中相应属性的值)
5、.sync
修饰符
在Vue1.x中,提供了.sync
修饰符。上面中提到,vue中子组件与父组件通信的方式为子组件给父组件emit一个事件,父组件监听事件。而.sync
则可以对一个prop进行双向绑定,即当父组件传给子组件一个prop时,当子组件改变了这个prop对应的状态后,那么父组件里的状态也被改变。如:
父组件中:
<subcom :someprop.sync="someState"></subcom>
当在子组件中改变了someprop
对应状态的时候,父组件中someState也会随之变化。
但是这个特征在vue2.0中移除了,又在vue2.3中重新引入,但实际上vue2.3中.sync
只是一个语法糖,它实际上是做了以下的事情:
<comp :foo.sync="bar"></comp>
会被扩展为:
<comp :foo="bar" @update:foo="val => bar = val"></comp>
这种情况下,当子组件需要更新foo
的值时,需要显示地触发:
this.$emit('update:foo', newValue)
6、v-model只是一个语法糖
对于<input v-model="something">
来说,它实现的是input数据和js中数据的双向绑定,而这种写法实际上是一种语法糖,它相当于:
<input
v-bind:value="something"
v-on:input="something = $event.target.value"
>
而同样的,当我们在一个组件里写了:
<comp v-model="something"></comp>
实际上也相当于:
<comp
v-bind:value="something"
v-on:input="something = arguments[0]"
></comp>
所以要让一个自定义组件的v-model
生效,那么这个组件需要满足:
- 接受一个
value
属性 - 在有新的值输入时,触发
input
事件,更新和value属性绑定的那个值
7、定制组件的v-model
通常情况下,一个组件的v-model
会使用value属性和input事件,但某些情况下,如checkbox和radio,它们的value
属性可能用作了其他目的,那么这种情况下就可以使用model
选项来定制v-model。
原生的vue中,checkbox中v-model
是和一个数组进行绑定的,但是现在我们希望有另外的一种checkbox,其v-model
和其绑定的属性的Boolean值挂钩,那么,实现如下:
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
/*
这么写之后,<my-checkbox v-model="_v" /> 就相当于:
<my-checkbox :checked="_v" @change="val => { _v = val }" />
*/
},
props: ['checked'],
template: '<input type="checkbox" :checked="checked" @click="update($event)" />',
methods: {
update($event) {
this.$emit('change', $event.target.checked);
}
}
});
8、非父子组件通信
当两个组件不是父子组件关系,但是又需要进行通信的时候,在简单场景下,可以这么做:
let bus = new Vue();
// 组件A
bus.$emit('eventName', carriedData);
// 组件B
bus.$on('eventName', function(carriedData) {
// ...
});
更复杂情况下,则应该使用vuex
来进行状态管理
9、内容分发
Vue中使用slot
来实现内容分发,内容分发就是子组件模板的部分内容,不由子组件自身提供,而是由父组件来传递,从而实现父子组件更灵活的组合。例子如:
匿名slot
父组件模板:
<div>
<h1>Headline</h1>
<child>
<p>这些内容是传递给子组件的</p>
</child>
</div>
那么,子组件child中如何获得<p>这些内容是传递给子组件的</p>
这段内容呢?那么,子组件的模板可以如:
<div>
<p>子组件自己的内容</p>
<slot></slot>
</div>
那么,组合后,子组件就会渲染为:
<div>
<p>子组件自己的内容</p>
<p>这些内容是传递给子组件的</p>
</div>
最后组合为:
<div>
<h1>Headline</h1>
<div>
<p>子组件自己的内容</p>
<p>这些内容是传递给子组件的</p>
</div>
</div>
具名slot
具名slot可以实现更精准的内容分发,如:
父组件模板:
<child>
<h1>Hello, world!</h1>
<template slot="header"><p>传给子组件的头部</p></template>
<template slot="footer"><p>传给子组件的尾部</p></template>
</child>
子组件模板:
<div>
<p>我是一个子组件</p>
<header>
<slot name="header"></slot>
</header>
<slot></slot>
<footer>
<slot name="footer"></slot>
</footer>
</div>
那么,组合的时候,<slot name="header"></slot>
会被以下代码所替换:
<p>传给子组件的头部</p>
而<slot name="footer"></slot>
则被以下代码所替换:
<p>传给子组件的尾部</p>
而匿名的slot,则由排除掉具名slot后剩余的内容所替换,为:
<h1>Hello, world!</h1>
所以最终会渲染为:
<div>
<p>我是一个子组件</p>
<header>
<p>传给子组件的头部</p>
</header>
<h1>Hello, world!</h1>
<footer>
<p>传给子组件的尾部</p>
</footer>
</div>
作用域插槽
作用域插槽允许子组件在获得父组件传递的内容的同时,可以给父组件传递的内容运用自身的属性。例子如下:
父组件模板:
<div class="parent">
<child>
<h1>Hello, world!</h1>
<template scope="item">
<span>{{item.title}}</span>
</template>
</child>
</div>
子组件模板:
<div class="child">
<p>我是一个子组件</p>
<slot title="Message from child"></slot>
<!-- 这里子组件中把title属性传递给父组件 -->
</div>
所以,子组件中的slot部分会被父组件中的下列部分替换:
** 这里,只有包裹在<template scope="item"></template>
内的内容有效,所以是:
<span>{{item.title}}</span>
从而得到:
<div class="child">
<p>我是一个子组件</p>
<span>{{item.title}}</span>
</div>
而其中的item.title
将被替换为Message from child
,从而得到:
<div class="child">
<p>我是一个子组件</p>
<span>Message from child</span>
</div>
因此最终渲染结果为:
<div class="parent">
<div class="child">
<p>我是一个子组件</p>
<span>Message from child</span>
</div>
</div>
此外,作用域插槽
还可以是具名的,通常的作用是用作列表组件,允许父组件来定义如何渲染列表的每一项,如:
父组件中:
<mylist>
<template slot="item" scope="props">
<li class="listItem">{{props.title}}</li>
</template>
</mylist>
子组件中:
<ul>
<slot name="item" v-for="item in items" :title="item.title"></slot>
</ul>
10、动态组件
可以通过<component>
元素,让多个组件使用同一个挂载点,动态切换,如:
<component v-bind:is="currentView"></component>
其中JS部分为:
let vm = new Vue({
el: '#app',
data: {
currentView: 'home'
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
keep-alive
如果希望把切换出去的组件保留在内存中,保留其状态或者避免重新渲染,可以使用keep-alive
,如:
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
11、其他杂项
1)可复用组件
Vue中组件的API主要来自于三个部分:
Props
允许外部环境传递数据给组件Events
允许组件传递数据给外部组件Slots
允许外部组件将额外的内容组合在组件中
2)子组件索引ref
可以使用ref
为子组件指定一个索引ID,如:
<child ref="childCom"></child>
然后父组件中可以通过以下方式访问子组件:
this.$refs.childCom
当ref
和v-for
一起使用时,ref
是一个数组,包含相应的子组件。此外,应该注意的是:
$refs
是在组件渲染完成后填充的,且是非响应式的
3)递归组件
组件在它的模板内可以递归调用自己,但是需要有name
选项时才可以,如:
name: 'my-comp'
不过,当我们使用Vue.component
全局注册一个组件的时候,全局的ID就会作为name
选项被自动设置。使用递归的时候应该特别注意:要确保递归调用有终止条件,否则会引起死循环
4)内联模板
当为子组件设置inline-template
属性时,其内容将作为模板,而不是内容分发,如:
<child inline-template>
<div>
<p>这里的内容都会被编译为组件组件的模板</p>
<p>是不会作为父组件的分发内容的</p>
</div>
</child>
即child
组件的template
选项将被指定为:
<div>
<p>这里的内容都会被编译为组件组件的模板</p>
<p>是不会作为父组件的分发内容的</p>
</div>
5)X-Templates
也可以使用X-Templates
来定义模板,如:
<script type="text/x-templates" id="app">
<p>Hello!!</p>
</script>
Vue.component('my-comp', {
template: '#app'
})
6)推荐对低开销的静态组件使用v-once
当组件中包含大量的静态内容时,可以使用v-once
将渲染结果缓存起来,如:
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static contents ...
</div>
`;
});