组件(Component)是可复用的Vue实例,这句话给了我们两个信息,可复用
和Vue实例
。可复用就是能够重复使用。前面的文章中,我们通过new的方式来创建Vue的根实例,它是整个应用的入口点。而组件是以标签的形式嵌入到HTML模板中,Vue在解析模板时会实例化该组件。
组件是资源独立的,组件在系统内部可复用,组件和组件之间也可以嵌套。
1 注册组件
组件分为全局注册和局部注册两种,两者的区别在于:
- 使用范围不同,全局注册的组件可在任何vue实例中使用,局部注册的组件只在注册该组件的vue实例的作用域范围内有效。
- 注册方式不同,全局注册使用vue全局API
Vue.component()
方法注册,局部注册是在vue实例中使用components
选项注册。
从使用范围可以得出,更通用的组件一般用全局注册,而特定场景的业务组件一般使用局部注册。
1.1 全局注册
全局注册使用的是Vue.component
全局API,代码如下:
Vue.component('my-component',{
//vue实例选项
});
全局注册方法有两个参数,第一个参数是组件的名称,第二个参数为组件的参数选项。组件名称有kebab-case(烤串样式,使用短横线分隔)和PascalCase(帕斯卡拼写法,首字母大写,单词开头大写)2种命名方式,推荐使用kebab-case的方式定义组件的名称。Vue实例通过这个名称引用该组件。
组件的参数选项与前面介绍的根实例的参数选项基本相同,什么data,methods,computed,watch等等,不过还是有部分差别:
- el选项,el选项作为Vue的挂载点,只有根实例才有
- data选项,在根实例中我们是以对象的形式定义,而在组件中,必须以函数的方式返回数据对象,原因在于组件是可复用的,也就是说,一个组件可以创建多个实例,如果data还是和根实例一样,以对象的形式定义,那么多个实例将引用同一个数据对象,当一个实例修改了data中的数据,其他实例中的data数据都会被修改,从而造成意想不到的结果。而使用函数的方式来返回对象,确保了每个组件实例的数据对象都是一个全新的副本数据。使用函数方式返回数据对象时,也不能返回一个外部对象的引用,原因同上。
<body>
<div id="app">
<my-component></my-component>
</div>
<script type="text/x-template" id="globalComponent">
<div>这是全局注册组件的内容:{{msg}}</div>
</script>
<script>
//全局组件组成
Vue.component('my-component',{
template:"#globalComponent",
//data数据选项必须使用函数的形式返回数据对象
data(){
return {
msg:"Hello World!"
}
}
});
var vm = new Vue({
el:"#app",
});
</script>
</body>
1.2 局部注册
局部注册使用实例的components选项,其后接对象,对象的键是组件的名称,值为组件的参数对象,定义如下:
<div id="app">
<component-a></component-a>
<component-b></component-b>
</div>
<script>
var componentA = {/** 组件的数据选项*/}
var componentB = {/** 组件的数据选项*/}
var vm = new Vue({
el:"#app",
//键是局部组件的名称,值为局部组件的参数选项
components:{
component-a:componentA,
component-b:componentB,
}
})
</script>
在使用ES6中,可以使用属性的简洁表示法,在对象中直接写入变量或函数,属性名就是变量名,属性值就是变量的值。
new Vue({
components:{
componentA,
componentB
}
})
2 递归调用组件
不论组件是全局注册还是局部注册,都可以实现递归调用。在说递归之前,需要介绍vue的name选项,name选项只能用在组件中,它在根实例中不起作用。组件使用name选项后,可以理解为该组件在其内部为自己定义了一个名称。
上面我们提到的全局注册函数Vue.component(id,{})
中id,以及局部注册components对象的键,他们也是组件的名称,只不过这个名称用于组件的外部调用。全局组件设name后,当递归调用自己时,可以使用外部名称,也可使用内部名称。
<body>
<div id="app">
<g-component :menus="menus"></g-component>
</div>
<!--定义全局组件模板-->
<script id="compo-ui" type="text/x-template">
<ul>
<li>{{menus.name}}</li>
<template v-if="hasChildren">
<!-- 使用内部名称调用自己 -->
<compo v-for="item in menus.children" :menus="item"></compo>
<!-- 也可以使用外部名称调用自己 -->
<!-- <g-component v-for="item in menus.children" :menus="item"></g-component> -->
</template>
</ul>
</script>
<script>
var params = {
//定义内部名称
name: "compo",
template: "#compo-ui",
props: ["menus"],
computed: {
hasChildren: function () {
let { children } = this.menus;
return (children && children instanceof Array && children.length > 0);
}
},
}
//组成全局组件,g-component是外部名称
Vue.component('g-component',params);
var vm = new Vue({
el: "#app",
data: {
menus: {
name: "总公司",
children: [
{name: "分公司1", children: [ { name: "部门1" }, { name: "部门1" },]},
{name: "分公司2", children: [ { name: "部门1" }, { name: "部门1" },]},
{name: "分公司3"}
]
},
},
});
</script>
</body>
3 内置组件
Vue为我们提供了5个内置组件,这5个组件我们可以直接使用,分别是:
组件 | 说明 |
---|---|
component | 动态组件,相当于一个占位符,根据条件动态的渲染一个组件 |
transition | 单个组件的过度效果 |
transition-group | 一组组件的过度效果 |
keep-alive | 保持其子组件的状态 |
slot | 内容分发插槽 |
本节我们将介绍component、keep-alive和slot内置组件,而关于动画效果的组件后续在讨论。
3.1 动态组件component
component组件时本身不会被渲染,它会根据条件动态的选择要渲染的组件。一般我们在定义组件时,会通过props来接收外部的数据,component这个内置组件也有两个props:
- is 表示被渲染的组件,可以是被渲染组件在注册时的名称,或者是定义组件的选项参数对象
- inline-template表示是否为内敛模板,很少用到
<body>
<div id="app">
<button @click="btnChangeClicked">切换</button>
<!-- component组件不会被渲染,真正被渲染的是currentComponent指向的组件 -->
<component :is="currentComponent" data="compb Title" @clicked="compClicked"></component>
</div>
<script>
var compb = {
template: "<h3 @click='clicked'>{{data}}</h3>",
props:["data"],
methods:{
clicked(){
this.$emit('clicked')
}
}
}
var vm = new Vue({
el: "#app",
data:{
currentComponent:"compa",
},
components:{
compa:{template:"<h3>组件A</h3>"},
compb
},
methods:{
btnChangeClicked(){
this.currentComponent=this.currentComponent=="compa"?"compb":"compa";
},
compClicked(){
console.log("动态组件也可以接收事件")
}
}
});
</script>
</body>
动态组件可以向普通的组件一样,传值和发送/接收事件。
3.2 动态组件状态保持
上面说了动态组件会根据条件,选择渲染那个组件。当从组件A切换到组件B时,组件A会被销毁,再次切换到组件A时,它又会被重新创建。这一点上,他和v-if动态渲染组件是一样的。
vue使用keep-alive内置组件来保证非活动状态的组件不会被销毁,并保留了它的状态。
<div id="app">
<button @click="btnChangeClicked">切换</button>
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
</div>
<script>
var compb = {
template: "<div><input type=’text‘></div>",
created() {
console.log("组件B被创建");
},
mounted() {
console.log("组件B被挂载");
},
destroyed() {
console.log("组件B被销毁");
}
}
...
</script>
上面的例子中,第一次切换到B时,由于没有B组件,所以会执行created和mounted钩子函数。因为keep-alive内置组件,所以,当切换到A时,B组件并不会调用destoryed方法,即B组件没有被销毁。再次切换到B时,不会调用created和mounted方法。
Keep-alive有以下几个props:
- include 要被缓存的组件名称,表达式的值可以是字符串、数组或是正则表达式。当为字符串时,使用逗号隔开。名称首先考虑内部名称(name选项),如果没有内部名称,则使用组件外部名称。内部名称优先级高于外部名称。
- exclude 不被缓存的组件名称,表达式的值类型同上
- max 最大缓存多少个组件,当缓存的组件达到最大值后,vue会按章最近访问顺序,将最远没有被访问的组件从缓存队列中删除,此时,被移除的组件会被销毁。
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
3.3 插槽slot
slot(插槽)组件可以理解为html模板中的占位符,它会被父组件传递过来的内容替换,这一过程叫做内容分发。这里的html模板是子组件中的html模板,而内容是父组件中传递过来的。slot组件中也可定义内容,如果父组件没有内容分发到子组件,那么slot的内容会被渲染。
<div id="app">
<button @click="count++">父组件点我</button>
<child-compo>
<p>替换子组件child-compo中的slot内置组件</p>
<p>你点击了 {{count}} 次</p>
</child-compo>
</div>
<script type="text/template" id="childCompoUI">
<div>
<slot>
<h4>我是slot的内容,如果父组件没有内容分发,会渲染我,如果有,会替换我</h4>
</slot>
</div>
</script>
<script>
var childCompo = {
template: "#childCompoUI",
};
var vm = new Vue({
el: "#app",
components:{
childCompo
},
data:{
count:0,
}
});
</script>
使用slot插槽,父组件要分发的内容包裹在子组件的标签中,这种写法与前面提到的子组件的props,props是以标签属性的形式存在。
3.3.1编译作用域
上面的例子中,父组件的分发的内容会替换子组件的slot标签。根据我们正常的理解,这个程序会报错, 因为分发的内容中有一个插值表达式{{count}}
,它读取了count的值,但是在子组件中,并没有定义count数据项,不过实际上,这个程序是可以正常运行的,原因就在于编译作用域。
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
也就是说,虽然分发的内容会替换子组件的slot,但是其作用域还是在父组件中,所以count是对应父组件中的内容。通过编译作用域,我们可以推断出,父组件分发的内容是不能读取子组件的数据,如下:
<!-- 程序会报错,父组件中没有定义message -->
<div id="app">
<child-compo>
<p>我想读取子组件的数据: {{message}} ,不过我读取不了</p>
</child-compo>
</div>
<script>
var childCompo = {
template: "#childCompoUI",
data(){
return{
message:"我是子组件的消息"
}
}
};
</script>
3.3.2具名插槽
在子组件中的html模板中,可以定义多个slot标签,比如,我们在子组件中定义一个结构,内容由父组件来分发。那么父组件如何找到对应的插槽呢?我们可以通过为slot组件设置一个名字,父组件通过slot的名字分发内容。具有名字的slot组件我们称之为具名插槽。
slot组件通过name属性设置名称。前面我们在定义slot时,没有设置name,Vue会默认给name加上一个default名字,这种插槽也叫默认插槽。父组件中推荐使用v-slot指令的参数部分指定插槽的名称,也可以在标签中使用slot属性指定插槽名,使用slot属性这种方式在vue 2.6版本后被废弃,不推荐这种方式。不过对于以前的代码,我们还是需要看得懂这种写法。
<body>
<div id="app">
<child-compo>
<!-- 使用v-slot指定具名插槽 -->
<template v-slot:header>
<h4>系统提示</h4>
</template>
<p>你确定要删除XX?</p>
<!-- v-slot简写为#,v-bind简写为:,v-on简写为@ -->
<template #footer>
<button>取消</button>
<button>确定</button>
</template>
</child-compo>
<!-- slot标签属性的写法已被废弃,能看懂就行 -->
<!-- <child-compo>
父组件通过slot属性指定具名插槽
<h4 slot="header">系统提示</h4>
<p>你确定要删除XX?</p>
<div slot="footer">
<button>取消</button>
<button>确定</button>
</div>
</child-compo> -->
</div>
<!--子组件定义-->
<script type="text/template" id="childCompoUI">
<div class="container">
<header>
<!--具名插槽,名称为header-->
<slot name="header"></slot>
</header>
<main>
<!--默认插槽,默认名称为default-->
<slot></slot>
</main>
<footer>
<!--具名插槽,名称为footer-->
<slot name="footer"></slot>
</footer>
</div>
</script>
<script>
var childCompo = {
template: "#childCompoUI",
};
var vm = new Vue({
el: "#app",
components: {
childCompo
}
});
</script>
</body>
这里需要注意2点:
- v-slot指令只能加在
<template>
标签上。 - 父组件中要分发的内容如果没有指定具名插槽,会将内容分发到默认插槽中,上面的
<p>你确定要删除XX?</p>
会替换子组件html模板中main的slot。
3.3.3 作用域插槽
上面我们在谈编译作用域的时候说了,父组件分发的内容是不能读取子组件的数据的。但是有些时候,父组件需要读取子组件的数据。这时,可以使用作用域插槽,作用域插槽是将子组件的数据通过v-bind指令绑定到slot上,父组件通过v-slot指令获取子组件的数据。
- 一个slot既可以是作用域插槽也可以是具名插槽。
- 2.6版本之前使用scope的方式获取作用域插槽的数据,该方式已被废弃
<body>
<div id="app">
<child-compo>
<!-- v-slot指令参数部分指定了具名插槽。v-slot指令的表达式部分,定义了作用域插槽绑定数据对象的名称(作用域插槽将绑定数据封装到一个对象中,这个名称就是对象的引用),这里是tmp -->
<template v-slot:header="tmp">
<h4>{{tmp.greeting}} {{tmp.username}}</h4>
</template>
</child-compo>
</div>
<script type="text/template" id="childCompoUI">
<div class="container">
<header>
<!-- 该插槽既是具名插槽又是作用域插槽,它绑定了两个数据,绑定的属性名可以任意取名,这个属性名被父组件中的使用 -->
<slot name="header" :greeting="message" :username="userName"></slot>
</header>
</div>
</script>
<script>
var childCompo = {
template: "#childCompoUI",
data(){
return {
userName:"张三",
message:"晚上好!"
}
}
};
var vm = new Vue({
el: "#app",
components: {
childCompo
}
});
</script>
</body>
这里使用v-slot:header="tmp"
指令指定了插座的,并获取插座内部的数据(使用tmp引用)。更进一步,我们可以使用es6的解构赋值将tmp这个中间变量去掉:
<template v-slot:header="{greeting, username}">
<h4>{{greeting}} {{username}}</h4>
</template>