一、组件概述
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>