一、组件概述
1.什么是组件?
组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。

所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。
二、组件定义
1、组件定义步骤
组件定义2步骤:
a、创建组件构造器
Vue.extend()定义要渲染的HTML
b、注册组件
Vue.component()定义组件名和构造器
调用Vue.extend()创建的是一个组件构造器,构造器有一个选项对象,选项对象的template属性用于定义组件要渲染的HTML;
调用Vue.component()注册组件时,需要提供2个参数:组件的标签名 和 组件构造器;注册的组件要挂载到某个Vue实例下,否则它不会生效;
Vue.extend() 和 Vue.component():由于 Vue 本身是一个构造函数,Vue.extend() 是一个类继承方法,它用来创建一个 Vue 的子类并返回其构造函数; 而Vue.component() 的作用在于:建立指定的构造函数与 ID 字符串间的关系,从而让 Vue.js 能在模板中使用它;直接向 Vue.component() 传递 options 时,它会在内部调用 Vue.extend()。
2、全局组件
全局组件定义2个步骤:
// 1.创建组件构造器
let Profile = Vue.extend({
// 1.1 模板选项
template: `
<div>
<input type="date">
<p>今天已经是冬天了!</p>
</div>
`
});
// 2. 注册一个全局的组件,任何实例都可以使用它
Vue.component('my-date', Profile);
全局组件可以直接在每一个vue实例中使用。
3、局部组件
// 1.创建组件构造器
let Profile = Vue.extend({
// 1.1 模板选项
template: `
<div>
<input type="date">
<p>今天已经是冬天了!</p>
</div>
`
});
//2、注册到一个指定的Vue实例中,这样就只能在某一个实例中使用这个组件了
new Vue({
el: '#app',
//通过注册到特定的实例,来达到局部应用,本例中只能应用在id=app的实例中
components: {
'my-date': Profile,
}
});
注意:
-
全局组件需要单独注册;
-
局部组件必须注册到特定的实例中,以components属性的方式;
三、父子组件
// 1. 子组件构造器
let Child1 = Vue.extend({
template: `<img src="img/img_01.jpg" width="200">`
});
let Child2 = Vue.extend({
template: `<p>我认为自己很美!</p>`
});
//如果想单独使用子组件,必须单独注册。这样就可以作为一个单独的全局组件使用。
Vue.component('child', Child1);
// 2. 父组件构造器
Vue.component('parent', {
components: {
'my-child1': Child1,
'my-child2': Child2
},
template:
`
<div>
<my-child1></my-child1>
<my-child2></my-child2>
</div>
`
});
//3、使用
<div id="app">
<parent></parent>
//child为全局组件,可以单独使用!
<child></child>
</div>
四、组件简化
1、template方式简化
<body>
<div id="app">
<my-div></my-div>
</div>
<template id="my_div">
//template里面只能有一个根,也就是所有内容都包含在一个div里面!
<div>
<div>我是MT!</div>
<input type="date">
<img src="img/img_02.jpg" alt="" width="400">
</div>
</template>
<script src="js/vue.js"></script>
<script>
// 1. 实例化组件
Vue.component('my-div', {
template: '#my_div'
});
new Vue({
el: '#app',
});
</script>
</body>
2、script方式简化
<body> <div id="app"> <my-div></my-div> </div> <script type="text/x-template" id="my_div"> <div> <img src="img/img_02.jpg" alt="" width="200"> <p>我是花姑娘!</p> </div> </script> <script src="js/vue.js"></script> <script> // 1. 实例化组件 Vue.component('my-div', { template: '#my_div' }); new Vue({ el: '#app', }); </script> </body>
注意事项:
1. 组件挂载数据必须是函数,返回一个对象
Vue.component('my-btn', {
template: '#my_btn',
data(){
return {
counter: 0
}
}
});
组件挂载数据,必须是一个函数,且返回一个对象,这样子组件被任何对象引用时,data就互不干涉,都是一个独立的对象,可以随时修改,数据都互不影响。
2、组件的template中,必须包含在一个根下面
<template id="my_div">
//template里面只能有一个根,也就是所有内容都包含在一个div里面!
<div>
<div>我是MT!</div>
<input type="date">
<img src="img/img_02.jpg" alt="" width="400">
</div>
</template>
<template id="my_div">
//这是错误的,不能有多个根元素!
<div>我是MT!</div>
<input type="date">
<img src="img/img_02.jpg" alt="" width="400">
</template>
五、组件间通讯
1、单层通讯
a、父子通信
<body>
<div id="app">
//第三步、父组件给子组件传值,单层传值无需动态绑定。父子传值,让子组件更活起来!
<my-div message="今天要下雨" imgsrc="img/img_02.jpg"></my-div>
<my-div message="明天要下冰雹" imgsrc="img/img_01.jpg"></my-div>
</div>
<template id="my_div">
//第一步:子组件定义接受的值
<div>
<h1>{{message}}</h1>
<img :src="imgsrc" width="200" alt="">
</div>
</template>
<script src="js/vue.js"></script>
<script>
// 1. 创建组件,
//第二步、通过props定义从父组件能够接受的值
Vue.component('my-div', {
template: '#my_div',
props: ['message', 'imgsrc']//注意,要支持驼峰式imgSrc的写法,需要在template和父组件里面修改成img-src或img-Src。
});
new Vue({
el: '#app',
data: {
msg: '今天的天气很好!'
}
});
</script>
</body>
2、多层通讯

<body>
//4、祖父组件使用和传值
<div id="app">
<my-parent :imgtitle="title" :imgsrc="img"></my-parent>
</div>
<template id="my_img">
<img :src="imgsrc" width="200">
</template>
<template id="my_title">
<h2>{{title}}</h2>
</template>
//3、父组件定义和传值
<template id="my_parent">
<div>
<child1 :childimg="imgsrc"></child1>
<child2 :childtitle="imgtitle"></child2>
</div>
</template>
<script src="js/vue.js"></script>
<script>
// 1. 子组件的实例
let Child1 = Vue.extend({
template: '#my_img',
props: ['childmyimg'] #父组件传递属性
});
let Child2 = Vue.extend({
template: '#my_title',
props: ['childtitle']
});
// 2. 子组件注册到父组件
Vue.component('my-parent', {
props: ['imgtitle', 'imgsrc'], #曾祖父传递的属性
components: {
'child1': Child1,
'child2': Child2
},
template: '#my_parent'
});
new Vue({
el: '#app',
data: {
title: '我是不是很漂亮',
img: 'img/img_01.jpg'
}
});
</script>
</body>
祖父组件=>父组件=>子组件,注意事项:
1、多层传值必须是动态绑定的方式 :
2、相对子组件必须通过props属性接受父组件传过来的数据
六、自定义事件

子组件通知给外界,他执行了什么操作,此时,外界会做出相应的操作。
<body>
<div id="app">
<!--父组件通过on监听total事件,一旦执行total事件,则会修改Vue实例中的数据,这样就达到了在父组件中多次调用子组件,数据都是独立的-->
<my-btn @total="allcounter()"></my-btn>
<my-btn @total="allcounter()"></my-btn>
<my-btn @total="allcounter()"></my-btn>
<my-btn @total="allcounter()"></my-btn>
<my-btn @total="allcounter()"></my-btn>
<my-btn @total="allcounter()"></my-btn>
<my-btn @total="allcounter()"></my-btn>
<p>所有按钮一共点击了{{totalCounter}}次</p>
</div>
<template id="my_btn">
<button @click="total()">点击了{{counter}}次</button>
</template>
<script src="js/vue.js"></script>
<script>
//定义子组件
Vue.component('my-btn', {
template: '#my_btn',
data(){
return {
counter: 0 //注意:在子组件中挂载数据,必须返回对象,这样在任何地方调用子组件都是独立的,数据互不干涉。
}
},
methods: {
total(){
this.counter += 1;
// 通知外界,我触发了total方法,然后他就会调用后面的allcounter(),这样就把所有的点击次数加起来了
this.$emit('total');
}
}
});
new Vue({
el: '#app',
data: {
totalCounter: 0
},
methods: {
allcounter(){
this.totalCounter += 1;
}
}
});
</script>
</body>
使用 $on(eventName) 监听事件
使用 $emit(eventName) 触发事件
七、插槽slot
slot的意思是插槽,其目的在于让组件的可扩展性更强。打个比方说:假如父组件需要在子组件内放一些DOM,那么这些DOM是显示、不显示、在哪个地方显示、如何显示,就是slot分发负责的。
注意:如果使用子组件时,在里面添加DOM,默认这些Dom是不会显示的,因为子组件根本不认识。例如:
<child><div>我想在子组件中显示,但是显示不出来</div></child>
插槽就像电路板,是用来方便插入内容的。slot分为匿名插槽和具名插槽。
匿名插槽就像公交车,谁都可以上,任何人都可以使用这个插槽,很类似娱乐圈中的女明星啦;
具名插槽则是私家车,一个人对应一辆车,一个内容对应一个插槽,这个就是生活中的良家妇女啦。
1、匿名插槽
<body>
<div id="app">
<my-slot>
<!--date会覆盖默认的插槽-->
<input type="date">
</my-slot>
</div>
<template id="my_slot">
<div id="panel">
<h2 class="panel-header">插槽的头部</h2>
<!--预留一个插槽-->
<slot>可以替换任何标签,默认显示提示的内容</slot>
<footer>插槽的尾部</footer>
</div>
</template>
<script src="js/vue.js"></script>
<script>
Vue.component('my-slot', {
template: '#my_slot'
});
new Vue({
el: '#app',
data: {
msg: '今天的天气很好!'
}
});
</script>
</body>
2、具名插槽
<body>
<div id="app">
<my-computer>
<!--只能根据slot的name插入对应的数据-->
<div slot="cpu">Inter Core i8</div>
<img slot="gpu" src="img/img_02.jpg" width="100" alt="">
<div slot="memory">128G</div>
<input type="color" slot="hard-drive">
</my-computer>
</div>
<template id="my_computer">
<div id="main">
<slot name="cpu">这里是插cpu的</slot>
<slot name="gpu">这里是插gpu的</slot>
<slot name="memory">这里是插内存条的</slot>
<slot name="hard-drive">这里是插硬盘的</slot>
</div>
</template>
<script src="js/vue.js"></script>
<script>
Vue.component('my-computer', {
template: '#my_computer'
});
new Vue({
el: '#app',
data: {
msg: '今天的天气很好!'
}
});
</script>
</body>
八、综合案例
组件以及父子组件传值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<div>
<input type="text" v-model="inputValue">
<button @click="handleClickBtn">添加</button>
</div>
<ul>
<todo-item
v-for="(item,index) in list"
:content="item"
:key="item"
:index="index"
:msg="msg"
@delete="handleParentDelete"
>
{{item}}
</todo-item>
</ul>
</div>
<script src="js/vue.js"></script>
<script>
//定义全局组件todoItem
var todoItem = {
//子组件接受来自父组件的传值,可以直接使用
props: ['content', 'index', 'msg'],
template: "<li @click='handleItemDelete'>{{content}}-{{msg}}</li>",
methods: {
handleItemDelete: function () {
//当点击子组件时,会向外触发delete事件,同时带上对应的参数,父组件监听到delete被触发后,则会执行对应的方法
this.$emit('delete', this.index)
}
},
}
// 1. 创建Vue的实例
let vm = new Vue({
el: '#app',
data: {
list:[],
inputValue:'',
msg:'我是来自父组件的msg'
},
components:{
//全局组件注册到vm实例中
todoItem
},
methods:{
handleClickBtn:function(){
if(this.inputValue){
this.list.push(this.inputValue);
this.inputValue = '';
}else{
alert('输入内容不能为空!');
}
},
handleParentDelete:function(id){
//$emit传给父组件的参数,在这里接受。
alert(id);
this.list.splice(id,1);
},
}
});
</script>
</body>
</html>
VUE的组件其实也是一个Vue实例,她具备vue的所有属性和方法。
九、组件的注意事项
1、is属性
解决h5的一些小bug
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<table>
<tbody>
//tbody标签直接使用row会在h5中有问题,解析后会跳到table外面去
<row></row>
<row></row>
<row></row>
//修改为:
<tr is="row"></tr>
<tr is="row"></tr>
<tr is="row"></tr>
//ul标签里面需要使用li,select标签中需要使用option
</tbody>
</table>
</div>
<script src="js/vue.js"></script>
<script>
Vue.component('row',{
template:'<tr><td>this is a row</td></tr>'
})
// 1. 创建Vue的实例
let vm = new Vue({
});
</script>
</body>
</html>
2、子组件的data
组件中的data必须是一个函数,返回一个对象
data:function(){
return {
number:0
}
},
3、ref使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<counter ref="one" @change="handleChange"></counter>
<counter ref="two" @change="handleChange"></counter>
<div>{{total}}</div>
</div>
<script src="js/vue.js"></script>
<script>
Vue.component('counter',{
template:'<div @click="handleClick">{{number}}</div>',
data:function(){
return {
number:0
}
},
methods:{
handleClick:function(){
this.number++
this.$emit('change')
}
}
})
// 1. 创建Vue的实例
let vm = new Vue({
el:'#app',
data:{
total:0
},
methods:{
handleChange:function () {
this.total = this.$refs.one.number + this.$refs.two.number
}
}
});
</script>
</body>
</html>
4、父子传值
父组件传值给子组件,子组件最好不要修改父组件的数据,因为加入父组件传递的是一个对象,可能被多个子组件反复的修改,导致数据混乱。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<counter :count='1' @change='handleChange'></counter>
<counter :count='2' @change='handleChange'></counter>
<span>{{total}}</span>
</div>
<script src="js/vue.js"></script>
<script>
var counter={
props:['count'],
template:"<div @click='handleClick'>{{number}}</div>",
data:function(){
return {
//拷贝一个父本出来
number:this.count
}
},
methods:{
handleClick:function(){
//注意:避免直接修改父组件传过来的数据
//this.count++
//每次步长为1
this.number += 1
//每次子组件被点击后都告知父组件触发对应的事件
this.$emit('change',1)
}
}
}
// 1. 创建Vue的实例
let vm = new Vue({
el: '#app',
components:{
counter
},
data:{
total:3
},
methods:{
handleChange:function(num){
this.total+=num
}
}
});
</script>
</body>
</html>
5、props和非props特性
子组件对父组件传过来的数据进行约束检测,则为props特性
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<child content="hello world"></child>
</div>
<script src="js/vue.js"></script>
<script>
Vue.component('child',{
props:{
content:{
type:String,
required:false,
default:'default value',
validator:function (value) {
return (value.length>5)
}
}
},
template:'<div>{{content}}</div>',
})
// 1. 创建Vue的实例
let vm = new Vue({
el:'#app',
});
</script>
</body>
</html>
props特点:
- 1、父组件传递过来
- 2、子组件必须验证
- 3、子组件可以使用
- 4、不会渲染在子组件中
父组件向子组件传递数据,但是在子组件没有明确接受,则不能通过{{}}使用,也称之为非props
非props特点:
- 1、父组件传递过来
- 2、子组件没明确写在props里面接受
- 3、子组件不能通过{{}}使用
- 4、会渲染在子组件当中,比如上面的content='hello wold‘会在子组件元素的html属性中渲染’。
6、给组件绑定原生事件
正常情况下,在父组件中绑定事件,我们只能在子组件通过this.$emit('events')进行触发后执行:
<child @click='handleClick'></child>
如果需要在父组件中也生效,需要通过给父组件绑定原生事件:
<child @click.native='handleClick'></child>
完整示例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<child @click.native='handleClick'></child>
</div>
<script src="js/vue.js"></script>
<script>
Vue.component('child',{
template:'<div>Child</div>',
})
// 1. 创建Vue的实例
let vm = new Vue({
el: '#app',
methods:{
handleClick:function(){
alert('aaa')
}
}
});
</script>
</body>
</html>