第7章 组件详情
7.1 组件与复用
7.1.1 为什么使用组件
组件的作用是为了代码可复用,提高重用性。在使用组件时可以自定义标签。比如:<Card>、<Row>、<i-col>等。
7.1.2 组件用法
组件需要注册后才能使用。注册分为全局注册和局部注册两种方式。全局注册后,任何Vue实例都可以使用。组件名自定义,最好使用小写加减号的形式命名。要是在父实例使用组件,必须要在实例创建前注册。
<div id="app">
<my-component></my-component>
</div>
<script>
Vue.component('my-component',{
//选项,需要显示的组件内容
template:'<div>组件内容</div>';
});
var app = new Vue({
el:'#app',
})
</script>
Template的DOM结构必须被一个元素包含,如果不带<div></div>标签是无法渲染的。
Vue实例中,使用component选项可以局部注册组件,注册的组件只在该Vue实例下有效。
<div id="app">
<my-component></my-component>
</div>
<script>
var Child = {
template:'<div>组件内容</div>'
}
var app = new Vue({
el:'#app',
components:{
'my-component':Child
}
})
</script>
Vue组件的模板受Html标签的限制,如<table>只允许<td>、<tr>、<th>等表格元素。所以直接在<table>内使用组件时无效的。这时,要使用特殊的is属性来挂载组件。
<div id="app">
<table>
<tbody is="my-component"></tbody>
</table>
</div>
提示:
除template选项外,还可以使用其他的选项,比如:data、computed、methods等。但在使用data时,和实例的区别是,data必须是函数,然后将数据return出去。
<div id="app">
<my-component></my-component>
</div>
<script>
Vue.component('my-component',{
//选项,需要显示的组件内容
template:'<div>{{message}}/div>',
data:function(){
return {
message:'组件内容'
}
}
});
var app = new Vue({
el:'#app',
})
</script>
注:javascript对象是引用关系,所以如果return出的对象引用了外部的一个对象,那这个对象就是共享的,任意一方修改皆会改变。
<div id="app">
<my-component></my-component>
<my-component></my-component>
<my-component></my-component>
</div>
<script>
var data={
counter:0
};
Vue.component('my-component',{
//选项,需要显示的组件内容
template:'<button @click="counter++">{{counter}}</button>',
data:function(){
return data;
}
});
var app = new Vue({
el:'#app',
})
</script>
组件使用了三次,每次操作时counter值都会改变,因为data引用了外部对象,可以用以下做法实现独立控制。
<div id="app">
<my-component></my-component>
<my-component></my-component>
<my-component></my-component>
</div>
<script>
Vue.component('my-component',{
//选项,需要显示的组件内容
template:'<button @click="counter++">{{counter}}</button>',
data:function(){
var data={
counter:0
};
}
});
var app = new Vue({
el:'#app',
})
</script>
7.2 使用props传递数据
7.2.1 基本用法
组件不仅仅要把模板内容复用,也需要实现组件间的通信。通常父组件会向子组件传递数据或参数,子组件通过获取父组件的数值不同渲染不同效果。这个正向传递就是通过props实现的。使用props声明需要从父组件中接收的数据,props可以分为字符串数组和对象两种。
<div id="app">
<my-component message="来自父组件的数据"></my-component>
</div>
<script>
Vue.component('my-component',{
props:{'message'},
template:'<div>{{message}}</div>',
});
var app = new Vue({
el:'#app',
})
</script>
Props中声明的数据和组件data函数return的数据主要区别在于props来自父级,而data中是组件自己的数据,作用域是组件本身。这两种数据都可以在模板template及计算属性computed和方法methods中使用。
由于html不区分大小写,当时使用DOM模板时,驼峰命名会被转化为短横分割命名。
当传递的值是动态赋值时,需要使用v-bind来绑定props值,当父组件数值变化时,子组件也会相应改变。
<div id="app">
<input type="text" v-model="parentMessage">
<my-component :message="parentMessage"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['message'],
template:'<div>{{message}}</div>',
});
var app = new Vue({
el:'#app',
data:{
parentMessage:''
}
})
</script>
7.2.2 单向数据流
Vue2.X通过props传递数据是单向的,父组件数值变化可以传递到子组件,但反向则不行。业务中经常会有两种要改变prop的情况。一种是父组件传递初始值进来,子组件将初始值保存起来,在自己的作用域随意修改。这种需要再组件data中声明数据,引用父组件的prop。
<div id="app">
<my-component :init-count="1"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['initCount'],
template:'<div>{{count}}</div>',
data:function(){
return{
count:this.initCount
}
}
});
var app = new Vue({
el:'#app'
})
</script>
还有种情况是prop作为需要被转变的原始值传入,这时需要使用计算属性。
<div id="app">
<my-component :width="100"></my-component>
</div>
<script>
Vue.component('my-component',{
props:['width'],
template:'<div :style="style">组件内容</div>',
computed:{
style:function(){
return{
width:this.width + 'px'
}
}
}
});
var app = new Vue({
el:'#app'
})
</script>
注意:在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以当props是对象或者数组时,在子组件内改变是会影响到父组件的。
7.2.3 数据验证
当需要验证时,可以使用对象写法实现,例如以下示例:
Vue.component('my-component',{
props:{
//数字类型
propA:Number,
//字符串或者数字类型
propB:[String,Number],
//布尔值,如果没有定义,默认true
propC:{
type:Boolean,
default:true,
},
//数字且必传
propD:{
type:Number,
required:true
},
//数组或对象,默认是一个函数返回
propE:{
type:Array,
default:function(){
return [];
}
},
//自定义验证函数
propF:{
validator:function(value){
return value > 10;
}
}
},
});
7.3 组件通信
7.3.1 自定义事件
当子组件向父组件传递数据时,要用到自定义事件。v-on除了监听DOM事件外,还可以用于组件间的自定义事件。子组件用$emit()来触发事件,父组件用$on()来监听子组件的事件。父组件也可以直接在子组件的自定义标签上使用v-on监听子组件触发的自定义事件。
7.3.2 使用v-model
Vue2.X可以在自定义组件上使用v-model命令。
<div id="app">
<p>总数:{{total}}</p>
<my-component v-model="total"></my-component>
</div>
<script>
Vue.component('my-component',{
template:'<button @click="handleClick">+1</button>',
data:function(){
return {
counter:0
}
},
methods:{
handleClick:function(){
this.counter ++ ;
this.$emit('input',this.counter);
}
}
});
var app = new Vue({
el:'#app',
data:{
total:0
}
})
</script>
v-model还可以创建自定义表单输入组件,进行数据双向绑定。
<div id="app">
<p>总数:{{total}}</p>
<my-component v-model="total"></my-component>
<button @click="handleReduce"></button>
</div>
<script>
Vue.component('my-component',{
props:['value'],
template:'<input :value="value" @input="updateValue">',
methods:{
updateValue:function(event){
this.$emit('input',event.target.value);
}
}
});
var app = new Vue({
el:'#app',
data:{
total:0
},
methods:{
handleReduce:function(){
this.total --;
}
}
})
</script>
注意:实现上述的双向数据绑定v-model需要满足下面两个要求:
- 接收一个value属性;
- 有新的value时触发input事件。
7.3.3 非父子组件通信
在实际业务中,除了父子组件之间需要通信外,还有兄弟组件和跨级组件之间。其中在Vue1.X中,除了$emit()外,还提供了$dispatch()和$broadcast(),其中$dispatch()是用于向上级派发事件,只要是它的父级,不论几级,都可以在实例的event选项中接收。$broadcast()是由上级向下级广播事件,用法一致,方向相反。
在Vue2.X中废弃了上面两个方法,使用一个空的Vue实例作为中央事件总线(bus),类似一个中介的作用。
<div id="app">
{{messagge}}
<component-a></component-a>
</div>
<script>
var bus = new Vue();
Vue.component('component-a',{
template:'<button @click="handleEvent">传递事件</button>',
methods:{
handleEvent:function(){
bus.$emit('on-messagge','来自组件component-a的内容');
}
}
});
var app = new Vue({
el:'#app',
data:{
messagge:''
},
mounted:function(){
var _this = this;
//在实例初始化时,监听bus实例的事件
bus.$on('on-messagge',function(msg){
_this.messagge = msg;
});
}
})
</script>
以上用法适用于任何组件之间的值传递。
除了中央事件总线bus外,还可以使用父链和子组件索引实现组件间的通信。
I、父链
在子组件中,使用this.$parent可以直接访问组件的父组件或实例,父组件也可以通过this.$children访问它所有的子组件。而且可以递归向上或向下无限访问,直到根实例或最内层的组件。但是在业务中,子组件尽量避免依赖父组件的数据,这样会造成父子耦合度过高,父子组件最好还是通过props和$emit来通信。
II、子组件索引
当子组件数量较多时,通过this.$children遍历是非常困难,尤其当动态渲染时,它们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称。
<div id="app">
<button @click="handleRef">通过ref获取子组件实例</button>
<component-a ref="comA"></component-a>
</div>
<script>
Vue.component('component-a',{
template:'<div>子组件</div>',
data:function(){
return {
messagge:'子组件内容'
}
}
});
var app = new Vue({
el:'#app',
methods:{
handleRef:function(){
//通过$refs访问指定的实例
var msg = this.$refs.comA.messagge;
console.log(msg);
}
}
})
</script>
注意:$refs只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用$refs。
7.4 使用slot分发内容
7.4.1 什么是slot
当需要组件组合使用时,混合父组件和子组件模板时,就会用到slot,这个过程就叫做内容分发。Props传递数据、events触发事件和slot分发内容构成了Vue组件的3个API来源。
7.4.2 作用域
父组件模板的内容是在父组件作用域内编译,子组件模板内容在子组件作用域内编译。
<div id="app">
<child-component v-show="showChild"></child-component>
</div>
<script>
Vue.component('component-a',{
template:'<div>子组件</div>',
});
var app = new Vue({
el:'#app',
data:{
showChild:true
}
})
</script>
这里showChild绑定的是父组件的数据,以下示例是在子组件上绑定:
<div id="app">
<child-component></child-component>
</div>
<script>
Vue.component('component-a',{
template:'<div v-show="showChild">子组件</div>',
data:function(){
return{
showChild:true
}
}
});
var app = new Vue({
el:'#app',
})
</script>
7.4.3 slot用法
单个slot
在子组件内使用特殊<slot>元素可以为这个子组件开启一个slot(插槽),在父组件里插入在子组件标签内的所有内容姜替代子组件的<slot>标签及它的内容。
<div id="app">
<child-component>
<p>分发内容</p>
<p>更多</p>
</child-component>
</div>
<script>
Vue.component(' child-component ',{
template:'\
<div>\
<slot>\
<p>如果父组件没有插入内容,默认显示</p>\
</slot>\
</div>',
});
var app = new Vue({
el:'#app',
})
</script>
子组件child-component模板内定义一个<slot>元素,并且有个<p>作为默认内容,在父组件没使用slot时,会渲染默认文本,如果写入slot,会替换整个slot。
<div id="app">
<div>
<p>分发内容</p>
<p>更多</p>
</div>
</div>
具名slot
给<slot>元素指定name可以分发多个内容,具名slot可以和单个slot共用。
<div id="app">
<child-component>
<h2 slot="header">标题</h2>
<p>分发内容</p>
<p>更多</p>
<div slot="footer">底部信息</div>
</child-component>
</div>
<script>
Vue.component('child-component',{
template:'\
<div class="container">\
<div class="header">\
<slot name="header"></slot>\
</div>\
<div class="main">\
<slot></slot>\
</div>\
<div class="footer">\
<slot name="footer"></slot>\
</div>
</div>',
});
var app = new Vue({
el:'#app',
})
</script>
子组件中有3个slot,其中class=”main”的slot没有使用name特性,它将作为默认slot出现。
<div id="app">
<div class="container">
<div class="header">
<h2>标题</h2>
</div>
<div class="main">
<p>分发内容</p>
<p>更多</p>
</div>
<div class="footer">
<div>底部信息</div>
</div>
</div>',
</div>
7.4.4 作用域插槽
作用域插槽是一个特殊的slot,使用一个可以复用的模板替换已经渲染的元素。
<div id="app">
<child-component>
<template>
<p>来自父组件的内容</p>
<p>{{props.msg}}</p>
</template>
</child-component>
</div>
<script>
Vue.component('child-component',{
template:'\
<div class="container">\
<slot msg="来自子组件的内容"></slot>\
</div>',
});
var app = new Vue({
el:'#app',
})
</script>
渲染后的结果:
<div id="app">
<div class="container">
<p>来自父组件的内容</p>
<p>来自子组件的内容</p>
</div>
</div>
7.4.5 访问slot
Vue2.X中提供了用来访问被slot分发内容的方法$slots。
mounted:function(){
var header = this.$slots.header;
var main = this.$slots.default;
var footer = this.$slots.footer;
console.log(footer);
console.log(footer[0].elm.innerHTML);
}
7.5 组件高级用法
7.5.1 递归组件
组件通过设置name的选项就可以递归的调用自己,如下:
<div id="app">
<child-component :count="1"></child-component>
</div>
<script>
Vue.component('child-component',{
name:'child-component',
props:{
count:{
type:Number,
default:1
}
},
template:'\
<div class="child">\
<child-component\
:count="count+1"\
v-if="count<3"></child-component>\
>
</div>',
});
var app = new Vue({
el:'#app',
})
</script>
7.5.2 内联模板
组件模板一般都是在template选项内定义的,Vue提供了内联模板的功能,在使用时,给组件标签使用inline-template特性,组件就会把它内容当成模板,而不是把内容做分发。
<div id="app">
<child-component inline-template>
<div>
<h2>在父组件中定义子组件的模板</h2>
<p>{{message}}</p>
<p>{{msg}}</p>
</div>
</child-component>
</div>
<script>
Vue.component('child-component',{
data:function(){
return {
msg;'在子组件声明的数据'
}
}
});
var app = new Vue({
el:'#app',
data:{
message:'在父组件声明的数据'
}
})
</script>
7.5.3 动态组件
Vue.js提供特殊元素<component>用来动态挂载不同的组件,使用is特性来选择挂载的组件。
<div id="app">
<component :is="currentView"></component>
<button @click="handleChangeView('A')">切换到A</button>
<button @click="handleChangeView('B')">切换到B</button>
<button @click="handleChangeView('C')">切换到C</button>
</div>
<script>
var app = new Vue({
el:'#app',
components:{
comA:{
template:'<div>组件A</div>'
},
comB:{
template:'<div>组件B</div>'
},
comC:{
template:'<div>组件C</div>'
},
},
data:{
currentView:'comA',
},
methods:{
handleChangeView:function(component){
this.currentView = 'com'+component;
}
}
})
</script>
7.5.4 异步组件
Vue.js允许将组件定义为一个工厂函数,动态解析组件,Vue.js只在组件需要渲染时触发工厂函数,并且把结果缓存起来。
<div id="app">
<component></component>
</div>
<script>
Vue.component('child-component',function(resolve,reject){
window.setTimeout(function(){
resolve({
template:'<div>异步渲染</div>'
});
},2000);
});
var app = new Vue({
el:'#app',
})
</script>
7.6 其他
7.6.1 $nextTick
异步更新队列:Vue观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一事件循环中发生所有数据改变。在缓冲时会去除重复数据避免不必要的计算。在下一个事件循环tick中,Vue刷新队列并执行实际工作。
当页面有v-if判断时,逻辑处理处理中会对相应的dom进行操作,但此时元素并未加载,页面就会报错,所以要使用$nextTick。$nextTick就是用来知道什么时候dom更新完成的。
7.6.2 X-Templates
当组件内容过长时使用拼接字符串是很麻烦的。Vue提供了另一种定义模板的方式,在<script>标签中使用text/template类型,并指定id赋值template。这样在<script>中可以写html代码,不需要考虑换行问题了。
<div id="app">
<my-component></my-component>
<script type="text/x-template" id="my-component">
<div>这是组件的内容</div>
</script>
</div>
<script>
Vue.component('my-component',{
template:'#my-component'
});
var app = new Vue({
el:'#app',
})
</script>
7.6.3 手动挂载实例
动态创建Vue实例,Vue提供了Vue.extend和$mount两个方法手动挂载实例。如果实例在实例化没有收到el选项,它就处于“未挂载”状态。可以通过$mount手动挂载实例。<
div id="mount-div">
</div>
<script>
var MyComponent = Vue.extend({
template:'<div>Hello:{{name}}</div>',
data:function(){
return{
name:'world'
}
}
});
new MyComponent().$mount('#mount-div');
</script>
除了上述写法还有两种写法:
new MyComponent(
el:'#mount-div'
});
//或者在文档之外渲染并且随后挂载
var component = new MyComponent().$mount();
document.getElementById('mount-div').appendChild(component.$el);