一、什么是组件? 什么是组件化?
组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is
特性进行了扩展的原生 HTML 元素。
简单的说: 组件就是把一个很大的界面拆分为多个小的界面, 每一个小的界面就是一个组件
将大界面拆分成小界面就是组件化
组件系统让我们可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用的界面都可以抽象为一个组件树:
组件化的好处
可以简化Vue实例的代码.
可以提高代码复用性
二、注册组件
全局注册
所有实例都能用全局组件。
1. 创建组件构造器
通过 全局API:Vue.extend()
参数:{Object} options
用法:使用基础 Vue 构造器,创建一个“子类”。参数是一个 包含组件选项的对象。
let Profile = Vue.extend({
// 注意: 在创建组件指定组件的模板的时候, 模板只能有一个根元素
template: `
<div>
<img src="images/fm.jpg" alt="">
<p>我是描述信息</p>
</div>
`
});
2. 注册已经创建好的组件
Vue.component('my-component', Profile);
3. 使用注册好的组件
<div id="app">
<my-component></my-component>
</div>
创建组件的简化方式
- 在注册组件的时候, 除了传入一个组件构造器以外, 还可以直接传入一个 对象
Vue.component('my-component', {
template: `
<div>
<img src="images/fm.jpg" alt="">
<p>我是描述信息</p>
</div>
`
});
- 在编写组件模板的时候, 除了可以在 字符串模板 中编写以外, 还可以像
art-template
一样在 script 中编写
<script id="info" type="text/html">
<div>
<img src="images/fm.jpg" alt="">
<p>我是描述信息</p>
</div>
</script>
- 在编写组件模板的时候, 除了可以在 script 中编写以外, vue还专门提供了一个编写模板的标签
template
<template id="info">
<div>
<img src="images/fm.jpg" alt="">
<p>我是描述信息</p>
</div>
</template>
上面两种编写模板的方式在创建的时候一定要记得加上 id
, 在使用的时候也要加上 id名称
Vue.component('my-component', {
template: '#info'
});
局部注册
我们也可以在实例选项中注册局部组件,这样组件只能在这个实例中使用.
可以通过某个 Vue 实例/组件的实例选项 components
注册仅在其作用域中可用的组件:
new Vue({
// ...
components: {
'my-component': {
template: '#info'
}
}
});
自定义全局组件特点:
在任何一个Vue实例控制的区域中都可以使用
自定义局部组件特点:
只能在自定义的那个Vue实例控制的区域中才可以使用
三、组件中的data和methods
Vue实例控制的区域相当于一个大的组件, 在大组件中我们可以使用data
和methods
而我们自定义的组件也是一个组件, 所以在自定义的组件中也能使用data
和methods
1. Vue中使用data和methods
<div id="app">
<button @click="vueFn">vue-alert</button>
<p>{{vueMsg}}</p>
</div>
new Vue({
el: '#app',
methods: {
vueFn(){
alert('vue-Fn');
}
},
data: {
vueMsg: 'vue-Msg'
}
});
2. 自定义组件中使用data和methods
在自定义组件中不能像在vue实例中一样直接使用data,而是必须通过返回函数的方式来使用data。
<template id="info">
<div>
<button @click="myFn">my-alert</button>
<p>{{myMsg}}</p>
</div>
</template>
Vue.component('my-component', {
template: '#info',
methods: {
myFn(){
alert('my-Fn');
}
},
data: function () {
return {
myMsg: 'my-Msg'
}
}
});
自定义组件中的data为什么是一个函数
因为自定义组件可以复用, 为了保证复用时每个组件的数据都是独立的, 所以必须是一个函数
看下面这个例子:
// HTML
<div id="app">
<my-component></my-component>
<my-component></my-component>
<my-component></my-component>
</div>
<template id="info">
<div>
<button @click="add">增加</button>
<p>{{counter}}</p>
</div>
</template>
// JS
Vue.component('my-component', {
template: '#info',
data: function () {
return {
counter: 0
}
},
methods: {
add(){
this.counter++;
}
}
});
new Vue({
el: '#app',
});
运行结果: 点击按钮的时候只有自己按钮下的数据会加1
组件中的data如果不是通过函数返回的, 那么多个组件就会共用一份数据, 就会导致数据混乱。
组件中的data如果是通过函数返回的, 那么每创建一个新的组件, 都会调用一次这个方法,将这个方法返回的数据和当前创建的组件绑定在一起, 这样就有效的避免了数据混乱。
如果 Vue 没有这条规则,点击一个按钮就会像影响到其它所有实例:那么上面的例子中的数据就会一起加1;
四、组件切换
1. 通过 v-if / v-else
对于普通的元素我们可以通过v-if来实现切换,对于组件我们也可以通过v-if来实现切换。
因为组件的本质就是一个自定义元素。
// HTML
<div id="app">
<button @click="show=!show">toggle</button>
<my-component1 v-if="show"></my-component1>
<my-component2 v-else></my-component2>
</div>
<template id="info1">
<div>
<p>我是info1</p>
</div>
</template>
<template id="info2">
<div>
<p>我是info2</p>
</div>
</template>
<script>
Vue.component('my-component1', {
template: '#info1'
});
Vue.component('my-component2', {
template: '#info2'
});
new Vue({
el: '#app',
data: {
show: true
}
});
</script>
2. 通过动态组件
通过v-if/v-else-if/v-else确实能够切换组件,但是在Vue中切换组件还有一种更专业的方式:
<component v-bind:is="需要显示组件名称"></component>
component我们称之为动态组件, 也就是你让我显示谁我就显示谁
通过使用 <component>
元素,动态地绑定到它的 is 特性
<div id="app">
<button @click="toggle">toggle</button>
<component :is="name"></component>
</div>
<template id="info1">
<div>
<p>我是info1</p>
</div>
</template>
<template id="info2">
<div>
<p>我是info2</p>
</div>
</template>
<script>
Vue.component('my-component1', {
template: '#info1'
});
Vue.component('my-component2', {
template: '#info2'
});
new Vue({
el: '#app',
data: {
show: true,
name: 'my-component1'
},
methods: {
toggle() {
this.show = !this.show;
this.name = this.name === 'my-component1' ? 'my-component2' : 'my-component1';
}
}
});
</script>
为什么可以通过v-if切换还要有component
因为component
可以配合keep-alive
来保存被隐藏组件隐藏之前的状态, 而v-if会重新渲染页面, 所以不能保存之前的状态
<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition>
相似,<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中
<keep-alive>
<component :is="name"></component>
</keep-alive>
<template id="info1">
<div>
<input type="checkbox">
<p>我是info1</p>
</div>
</template>
这里可以记住复选框的选中状态
如果我们需要频繁的切换页面,每次都是在组件的创建和销毁的状态间切换,这无疑增大了性能的开销。
这个时候我们也可以使用Vue提供了动态组件的 缓存。keep-alive
会在切换组件的时候缓存当前组件的状态,等到再次进入这个组件,不需要重新创建组件,只需要从前面的缓存中读取并渲染。
五、组件动画
给组件添加动画和过去给元素添加动画一样。
如果是单个组件就使用transition
,如果是多个组件就使用transition-group
。
// HTML
<div id="app">
<button @click="toggle">toggle</button>
<transition>
<component :is="name"></component>
</transition>
</div>
<template id="info1">
<div>
<p>我是info1</p>
</div>
</template>
<template id="info2">
<div>
<p>我是info2</p>
</div>
</template>
// CSS
.v-enter{
opacity: 0;
margin-left: 500px;
}
.v-enter-to{
opacity: 1;
}
.v-enter-active{
transition: all 3s;
}
.v-leave{
opacity: 1;
}
.v-leave-to{
opacity: 0;
}
.v-leave-active{
transition: all 3s;
margin-left: 500px;
}
// JS
<script>
Vue.component('my-component1', {
template: '#info1'
});
Vue.component('my-component2', {
template: '#info2'
});
new Vue({
el: '#app',
data: {
show: true,
name: 'my-component1',
},
methods: {
toggle(){
this.show = !this.show;
this.name = this.name === 'my-component1' ? 'my-component2' : 'my-component1';
}
}
});
</script>
效果:
在这个案例中可以发现一个问题: 两个的话是同时执行的.
默认情况下进入动画和离开动画是同时执行的, 如果想一个做完之后再做另一个, 需要指定动画的过渡模式.
过渡模式
过渡模式常常配合多个元素或者多个组件切换时使用,有如下两种模式:
-
in-out
:新元素先进行过渡,完成之后当前元素过渡离开。(默认为该模式) -
out-in
:当前元素先过渡离开,离开完成后新元素过渡进入。
所以可以把上面的代码改造一下, 就可以让一个元素先出去, 另一个元素再进来
<transition mode='out-in'>
<component :is="name"></component>
</transition>
六、父子组件
在一个组件中又定义了其它组件就是父子组件。
其实局部组件就是最简单的父子组件, 因为我们可以把Vue实例看做是一个大组件。
我们在Vue实例中定义了局部组件, 就相当于在大组件里面定义了小组件, 所以局部组件就是最简单的父子组件
1. 如何定义父子组件
前面讲过, 自定义组件中可以使用data
, 可以使用methods
. 当然自定义组件中也可以使用components
,
所以我们也可以在自定义组件中再定义其它组件。
- 在全局父组件中定义子组件
Vue.component('father', {
template: '#father',
components: {
'son': {
template: '#son'
}
}
});
- 在局部父组件中定义子组件
new Vue({
el: '#app',
components: {
'father': {
template: '#father',
components: {
'son': {
template: '#son'
}
}
}
}
});
-
父子组件的使用
把自定义子组件放到自定义父组件中, 把自定义父组件放到Vue组件中
<div id="app">
<father></father>
</div>
<template id="father">
<div>
<p>我是父组件</p>
<son></son>
</div>
</template>
<template id="son">
<div>
<p>我是子组件</p>
</div>
</template>
2. 父子组件数据传递
在Vue中子组件是不能访问父组件的数据的,如果子组件想要访问父组件的数据, 必须通过父组件传递。
如何传递数据
- 在父组件中通过
v-bind
传递数据
传递格式: v-bind:自定义接收名称 = "要传递数据"
<template id="father">
<div>
<p>{{msg}}</p>
<!--这里将父组件的msg通过parentmsg传递给了子组件-->
<son :parentmsg="msg"></son>
</div>
</template>
- 在子组件中通过
props
接收数据
接收格式: props: ["自定义接收名称"]
//...
components: {
'son': {
template: '#son',
// 这里通过parentmsg接收了父组件传递过来的数据
props: ['parentmsg']
}
}
如何使用数据
- 子组件使用父组件传递的数据
<template id="son">
<div>
<!--这里通过parentmsg使用了父组件传递过来的数据-->
<p>{{parentmsg}}</p>
</div>
</template>
3. 父子组件方法传递
在Vue中子组件是不能访问父组件的方法的,如果子组件想要访问父组件的方法, 必须通过父组件传递
如何传递方法
- 在父组件中通过
v-on
传递方法
传递格式: v-on:自定义接收名称 = "要传递方法"
<template id="father">
<div>
<button @click="say">父组件按钮</button>
<!--这里通过parentsay将父组件的say方法传递给了子组件-->
<son @parentsay="say"></son>
</div>
</template>
- 在子组件中自定义一个方法
<template id="son">
<div>
<button @click="sonFn">子组件按钮</button>
</div>
</template>
- 在自定义方法中通过
this.$emit('自定义接收名称');
触发传递过来的方法
components: {
'son': {
template: '#son',
methods: {
sonFn(){
this.$emit('parentsay');
}
}
}
}
和传递数据不同, 如果传递的是方法, 那么在子组件中不需要接收。
但是需要在子组件中自定义一个方法, 直接使用自定义的方法.
并且还需要在子组件自定义的方法中通过
this.$emit("自定义接收的名称")
的方法来触发父组件传递过来的方法
$emit( eventName, […args] ) 触发事件
- {string} eventName 需要调用的函数名称
- [...args] 给调用的函数传递的参数
触发当前实例上的事件。附加参数都会传给监听器回调。
所以子组件可以通过这个方法给父组件传递参数.
components: {
'son': {
template: '#son',
methods: {
sonFn(){
this.$emit('parentsay', 'son');
}
}
}
}
父组件接收参数:
methods: {
say(data){
console.log(data);
}
}
4. 数据和方法的多级传递
在Vue中如果儿子想使用爷爷的数据, 必须一层一层往下传递
在Vue中如果儿子想使用爷爷的方法, 必须一层一层往下传递
七、组件中的命名
1. 注册组件的时候使用了"驼峰命名", 那么在使用时需要转换成"短横线分隔命名"
例如: 注册时: myFather -> 使用时: my-father
2. 在传递参数的时候如果想使用"驼峰名称", 那么就必须写"短横线分隔命名"
例如: 传递时: parent-msg="msg" -> 接收时: props: ["parentMsg"]
3. 在传递参数的时候如果想使用"驼峰名称", 那么就必须写"短横线分隔命名"
例如: @parent-say="say" -> this.$emit("parent-say");
八、插槽
默认情况下使用子组件时,在子组件中编写的元素是不会被渲染的
如果子组件中有部分内容是使用时才确定的, 那么我们就可以使用插槽
插槽就是在子组件中放一个"坑", 以后由父组件来"填"。
比如在下面这个例子中, 没有使用插槽的话父组件在<son></son>
内编写的内容是无效的
<template id="father">
<div>
<son>
<!--如果没有插槽, 父组件在这里编写的内容是无效的-->
</son>
</div>
</template>
默认情况下是不能在使用子组件的时候, 给子组件动态的添加内容的
如果想在使用子组件的时候, 给子组件动态的添加内容, 那么就必须使用插槽
1. 匿名插槽
没有名字的插槽, 会利用使用时指定的内容替换整个插槽
这里的slot标签
就是插槽, 插槽其实就是一个坑。只要有了这个坑, 那么以后使用者就可以根据自己的需要来填这个坑。
<template id="son">
<div>
<div>我是头部</div>
<slot>我是默认的内容</slot>
<div>我是底部</div>
</div>
</template>
插槽可以指定默认数据, 如果使用者没有填这个坑, 那么就会显示默认数据。
如果使用者填了这个坑, 那么就会利用使用者坑的内容替换整个插槽。
例如: 这个father组件在使用son组件的时候填了这个坑,那么就会用父组件坑的内容覆盖掉整个插槽.
所以最后的效果是:
<template id="father">
<div>
<!--需求: 在使用子组件的时候给子组件动态的添加一些内容-->
<son>
<div>我是追加的内容</div>
</son>
</div>
</template>
匿名插槽的特点:
有多少个匿名插槽, 填充的数据就会被拷贝几份
<template id="son">
<div>
<div>我是头部</div>
<slot>我是默认的内容</slot>
<slot>我是默认的内容</slot>
<div>我是底部</div>
</div>
</template>
效果图:
虽然我们可以指定多个匿名插槽, 但是推荐只写一个匿名插槽
2. 具名插槽
默认情况下有多少个匿名插槽, 我们填充的数据就会被拷贝多少份,这导致了所有插槽中填充的内容都是一样的。
那么如果我们想给不同的插槽中填充不同的内容怎么办呢?
这个时候就可以使用具名插槽
具名插槽的使用
- 通过插槽的
name
属性给插槽指定名称
<template id="son">
<div>
<div>我是头部</div>
<slot name="one">我是one默认的内容</slot>
<slot name="two">我是two默认的内容</slot>
<div>我是底部</div>
</div>
</template>
- 在使用时可以通过
slot="name"
方式, 指定当前内容用于替换哪一个插槽
默认情况下填充的内容是不会被填充到具名插槽中的,
只有给填充的内容指定了要填充到哪一个具名插槽之后,
才会将填充的内容填充到具名插槽中
<template id="father">
<div>
<son>
<!--这里通过slot属性告诉Vue,当前的内容是要填充到哪一个插槽中的-->
<div slot="one">我是追加的内容one</div>
<!--如果没有指定要替换哪个插槽中的内容, 则不会被替换-->
<div>我是追加的内容two</div>
</son>
</div>
</template>
3. v-slot指令
v-slot
指令是Vue2.6中用于替代slot
属性的一个指令
在Vue2.6之前, 我们通过slot
属性告诉Vue当前内容填充到哪一个具名插槽
从Vue2.6开始, 我们通过v-slot
指令告诉Vue当前内容填充到哪一个具名插槽
格式: v-slot:插槽名称
简写: #插槽名称
注意: v-slot指令只能用在template标签上
例如:
<son>
<template v-slot:one>
<div>我是追加的内容one</div>
<div>我是追加的内容one</div>
</template>
<template #two>
<div>我是追加的内容two</div>
<div>我是追加的内容two</div>
</template>
</son>
4. 作用域插槽
作用域插槽就是带数据的插槽, 就是让父组件在填充子组件插槽内容时也能使用子组件的数据
如何使用作用域插槽
- 在
slot
中通过v-bind:数据名称="数据名称"
方式暴露数据
<template id="son">
<div>
<div>我是头部</div>
<!--v-bind:names="names"作用: 将当前子组件的names数据暴露给父组件-->
<slot v-bind:names="names"></slot>
<div>我是底部</div>
</div>
</template>
- 在父组件中通过
<template slot-scope="作用域名称">
接收数据
<template id="father">
<div>
<son>
<!--slot-scope="sonMsg"作用: 接收子组件插槽暴露的数据-->
<template slot-scope="sonMsg">
</template>
</son>
</div>
</template>
- 在父组件的
<template></template>
中通过 作用域名称.数据名称 方式使用数据
<template slot-scope="sonMsg">
<div>{{sonMsg.names}}</div>
</template>
作用域插槽的应用场景:
子组件提供数据, 父组件决定如何渲染
5. v-slot 指令代替 slot-scope
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。
它取代了 slot 和 slot-scope
也就是说我们除了可以通过 v-slot
指令告诉Vue内容要填充到哪一个具名插槽中,还可以通过v-slot
指令告诉Vue如何接收作用域插槽暴露的数据
格式: v-slot:插槽名称="作用域名称"
简写: #插槽名称="作用域名称"
<!--匿名插槽的默认名称是default-->
<template v-slot:default="sonMsg">
<div>{{sonMsg.names}}</div>
</template>