第11章:组件详解
组件是Vue.js最推崇也最强大的功能之一,核心目标是可重用性。
我们把组件代码按照template、style、script的拆分方式,放置到对应的.vue文件中。
1.注册
Vue.js的组件注册分为全局注册和局部注册。
全局注册使用Vue.component方法。第一个参数是组件名字,第二个参数是组件的构造函数,要么是function,要么是object。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <didi-component></didi-component> <en-component></en-component> </div> <script src="js/vue.js"></script> <script> //第二个参数传入object的情况 Vue.component("didi-component",{ template:"<div>A custom component!</div>" }); //第二个参数传入function的情况,它是一个组件构造器 var ENcomponent=Vue.extend({ template:"<div>A 2rd components</div>" }); //注册 Vue.component("en-component",ENcomponent); //创建根实例 new Vue({ el:"#app" }) </script> </body> </html>
显示效果
局部注册,以vue实例选项components或者组件构造器即Vue.extend函数的components选项。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <didi-component></didi-component> </div> <script src="js/vue.js"></script> <script> var Parent=Vue.extend({ template:"<p>这里是父组件的内容</p><br/><child></child>", components:{ "child":{ template:"<div>我是子组件的内容</div>" } } }) new Vue({ el:"#app", components:{ "didi-component":Parent } }) </script> </body> </html>
显示效果
2.数据传递
组件作为一个app(项目)的组成部分,总是要涉及到组件通信。组件通信属于数据传递的范畴,数据传递一共有3种方式:
props、组件通信、slot。
(1)props属性
首先区别一下props和data和propsData。props作用于父子组件之间的数据传递,data组件或者vue实例化时经常使用,propsData用来在组件初始化后覆盖props中的属性。接下来是如何使用props属性。
第一种方式,字面量语法
直接在组件的html代码中传入普通的字符串,在组件内部使用props属性。注意:html不区分大小写,名字为camelCase的属性会转化为短横线隔开(kebab-case)。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <hello-component msg="hello"></hello-component> <vivian my-data="3.1415"></vivian> </div> <script src="js/vue.js"></script> <script> Vue.component("hello-component",{ props:["msg"], template:"<span>{{msg}},front-end!</span>" }); Vue.component("vivian",{ props:["myData"], template:"<div>{{myData}}</div>", replace:true }) new Vue({ el:"#app" }) </script> </body> </html>
第二种,动态语法
下面的例子从父组件向子组件传递属性,每当父组件的数据变化时,该变化也会传导给子组件。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <vivi-props></vivi-props> </div> <script src="js/vue.js"></script> <script> var Child=Vue.extend({ props:["viviProps"], //传给子组件的属性名称 template:"<div>{{viviProps}}</div>", //既然在props属性上定义了,那么就可以在模板里使用了 replace:true }); var Parent=Vue.extend({ template:"<p>我是父组件的内容</p><br/><child :vivi-props=‘hello‘></child>", data:function(){ return {"hello":"hello"} }, //动态绑定的属性的值由父组件的data来定义 components:{ "child":Child } }) new Vue({ el:"#app", components:{ "vivi-props":Parent } }); </script> </body> </html>
第三种方式,使用.sync和.once修饰符。
默认或者使用.once修饰符是父组件到子组件的单向绑定。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="text" v-model="info.name"/> <child v-bind:msg.once="info"></child> <!--把根实例的info数据属性传递给子组件child的msg属性--> </div> <script src="js/vue.js"></script> <script> new Vue({ el:"#app", data:{ info:{ name:"顺风车" } }, components:{ "child":{ props:["msg"], template:"<div>{{msg.name}}</div>" } } }) </script> </body> </html>
.sync修饰符能做到双向绑定。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> 父组件: <input v-model="val"><br/> 子组件: <test :test.sync="val"></test> </div> <script src="js/vue.js"></script> <script> var vm = new Vue({ el: ‘#app‘, data: { val: 1 }, components: { "test": { props: [‘test‘], template: "<input v-model=‘test‘/>" } } }); </script> </body> </html>
显示结果
第4种方式,prop验证。
我们把自己的组件给别人使用,那么要求使用者能够根据prop提供的对象正确使用。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> 父组件: <input v-model="val"><br/> 子组件: <test :test="val"></test> </div> <script src="js/vue.js"></script> <script> var vm = new Vue({ el: ‘#app‘, data: { val: 1 }, components:{ test:{ props: { test: { twoWay: true } //我们希望组件的使用者通过双向绑定来使用这个组件。 }, //把根实例的val属性传递给test属性。 template: "<input v-model=‘test‘/>" } } }); </script> </body> </html>
我们看到这个代码 <test :test="val"></test> 会抛出错误如下:
那么正确的使用应该是如下,加上.sync修饰词,以满足twoWay:true的要求。
第5种方式,prop转换函数,coerce函数。可以理解为prop属性值的过滤器。
(2)组件通信
尽管this.$parent和this.$children(属性)可以访问父/子组件的数据,根实例的后代可以通过this.$root访问根实例,但这种方式应该尽量避免。
首先,因为父子组件紧密的耦合,只看父组件却很难理解父组件的状态,因为它能被任意的修改。再次,子组件要使用父组件的数据,应当避免直接依赖,尽量显式的使用props属性。
我们的方案是通过作用域传递事件。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <template id="child-template"> <input v-model="msg"> <button v-on:click="notify">Dispatch Event</button> <!--子组件绑定了notify函数--> </template> <div id="events-example"> <p>Message:{{messages|json}}</p> <child></child> </div> </div> <script src="js/vue.js"></script> <script> Vue.component("child",{ template:"#child-template", data:function(){ return {msg:"hello"} }, methods:{ notify:function(){ if(this.msg.trim()){ this.$dispatch("child-msg",this.msg); this.msg=""; } //notify函数的功能是把msg属性的值dispatch到child-msg事件上,它会沿着父作用域的链冒泡 } } }); var parent=new Vue({ el:"#events-example", data:{ messages:[] }, events:{ "child-msg":function(msg){ this.messages.push(msg); } //接收到child-msg事件后,那么执行push方法 } }) var vm = new Vue({ el: ‘#app‘ }); </script> </body> </html>
接下来是在子组件v-on监听,更加简洁的代码。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <template id="child-template"> <input v-model="msg"> <button v-on:click="notify">Dispatch Event</button> <!--子组件绑定了notify函数--> </template> <div id="events-example"> <p>Message:{{messages|json}}</p> <child v-on:child-msg="handleIt"></child> </div> </div> <script src="js/vue.js"></script> <script> Vue.component("child",{ template:"#child-template", data:function(){ return {msg:"hello"} }, methods:{ notify:function(){ if(this.msg.trim()){ this.$dispatch("child-msg",this.msg); this.msg=""; //notify函数的功能是把msg属性的值dispatch,触发child-msg事件,它会沿着父作用域的链冒泡 } } }); var parent=new Vue({ el:"#events-example", data:{ messages:[] }, //改动后的代码 methods:{ handleIt:function(msg){ this.messages.push(msg); } } }) var vm = new Vue({ el: ‘#app‘ }); </script> </body> </html>
有时js中直接访问子组件,可用v-ref为子组件指定一个索引ID。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <div id="events-example"> <p>Message:{{messages|json}}</p> <button @click="handleIt">打印msg字符串儿</button> <!--点击按钮,那么执行handleIt方法--> <child v-ref:id1></child> </div> </div> <script src="js/vue.js"></script> <script> Vue.component("child",{ template:"<div>{{msg}}</div>", data:function(){ return {msg:"hello"} } }); var parent=new Vue({ el:"#events-example", data:{ messages:[] }, //改动后的代码 methods:{ handleIt:function(){ console.log(this.$refs.id1.msg); //那么就可以直接通过id名访问到子组件的属性了 } } }) var vm = new Vue({ el: ‘#app‘ }); </script> </body> </html> </body> </html>
(3)slot分发内容
组合组件时,内容分发API是非常有用的机制。
<vivi> <vivi-header></vivi-header> <vivi-footer></vivi-footer> </vivi>
首先,明确分发内容的作用域。
<child v-on:child-msg="handleIt">{{msg}}</child>
msg绑定到父组件的数据还是子组件的数据里?答案是父组件。因为父组件模板的内容在父组件的作用域里编译,子组件模板的内容在子组件的作用域里编译。
所以对于分发内容(接下来就知道什么是分发内容了), 在父组件内容里编译。
接下来看,单个slot。
当子组件模板里只有一个没有任何修饰的Slot,那么父组件的整个内容将插入到slot的挂载点并替换掉它。
单个slot的DEMO:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <template id="viviTemplate"> <div> <h1>这里是vivi组件的内容</h1> <slot> 如果没有分发内容,则显示我 </slot> <!--回退内容,当宿主元素为空没有内容插入时--> </div> </template> <div id="app"> <vivi-component> <p>这里是原始内容</p> <p>这里是更多的原始内容</p> <!--很明显,这里的p标签的作用域并不是vivi-component的内容,而是它的父组件的内容--> </vivi-component> </div> <script src="js/vue.js"></script> <script> Vue.component("vivi-component",{ template:"#viviTemplate", }) var vm = new Vue({ el: ‘#app‘ }); </script> </body> </html> </body> </html>
显示效果:
接下来看,命名的slot的用法。
<slot>元素可使用name配置如何分发内容。仍然可以有一个匿名的slot,作为找不到匹配内容片段的回退容器。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <template id="multiTemplate"> <div> <slot name="one"></slot> <slot></slot> <slot name="two"></slot> </div> </template> <div id="app"> <multi-insertion> <p slot="one">One</p> <!--对应name值为one的被分发内容--> <p slot="two">Two</p> <!--对应name值为two的被分发的内容--> <p>Default A</p> <p slot="three"></p> <!--对应默认的被分发的内容--> </multi-insertion> </div> <script src="js/vue.js"></script> <script> Vue.component("multi-insertion",{ template:"#multiTemplate", }) var vm = new Vue({ el: ‘#app‘ }); </script> </body> </html> </body> </html>
显示结果:
最后分析一下,源码。
SLOT的源码如下:
var slot = { priority: SLOT,//优先级 params: [‘name‘],// //bind方法的功能是绑定指令 bind: function bind() { var name = this.params.name || ‘default‘; var content = this.vm._slotContents && this.vm._slotContents[name]; if (!content || !content.hasChildNodes()) { this.fallback(); } else { this.compile(content.cloneNode(true), this.vm._context, this.vm); } }, compile: function compile(content, context, host) { // if (content && context) { if (this.el.hasChildNodes() && content.childNodes.length === 1 && content.childNodes[0].nodeType === 1 && //对v-if指令的处理,略。 content.childNodes[0].hasAttribute(‘v-if‘)) { var elseBlock = document.createElement(‘template‘); elseBlock.setAttribute(‘v-else‘, ‘‘); elseBlock.innerHTML = this.el.innerHTML; elseBlock._context = this.vm; content.appendChild(elseBlock); } var scope = host ? host._scope : this._scope; this.unlink = context.$compile(content, host, scope, this._frag); } if (content) { replace(this.el, content); } else { remove(this.el); } // }, fallback: function fallback() { this.compile(extractContent(this.el, true), this.vm); //把元素的内容抽出来,然后编译 }, unbind: function unbind() { if (this.unlink) { this.unlink(); } // } };
先来看bind方法。这里的this指的是<slot>指令,通过name获取到分发的内容,比如 <p slot="one">One</p> 。有分发的内容,那么就添加到slot指令挂载的元素。没有就把slot指令所在元素删除掉。
3.混合
使用混合对象(把选项分离出来,然后组合进组件的选项中)。好处是方便分布复用功能。
第一,简单的混合例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <vivi-component></vivi-component> </div> <script src="js/vue.js"></script> <script> var myMixin={ created:function(){ this.hello(); }, methods:{ hello:function(){ console.log("hello from mixin!"); } } }; //myMixin要放在使用之前定义哦。顺序问题很重要。 new Vue({ el:"#app", components:{ "vivi-component":{ template:"<h1>hello ,vivian!</h1>", mixins:[myMixin] } } }) </script> </body>. </html>
第二,当混合对象与组件有同名选项时,以适当的策略合并。同名钩子函数被并入一个数组中,因而都会被调用。而且顺序是,混合的钩子函数比组件的钩子函数更早被调用。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>混合对象</title> </head> <body> <div id="app"> <vivi-component></vivi-component> </div> <script src="js/vue.js"></script> <script> var myMixin={ created:function(){ this.hello(); }, methods:{ hello:function(){ console.log("hello from mixin!"); } } }; new Vue({ el:"#app", components:{ "vivi-component":{ template:"<h1>hello ,vivian!</h1>", created:function(){ console.log("组件内容被调用"); }, mixins:[myMixin] } } }) </script> </body>. </html>
显示两个钩子函数都有被调用哦.
第三,methods、components、directives都是对象选项,合并时如果键冲突,那么组件的选项优先。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> </div> <script src="js/vue.js"></script> <script> var myMixin={ methods:{ foo:function(){ console.log("foo"); }, conflicting:function(){ console.log("from mixin"); } } }; var component=Vue.extend({ mixins:[myMixin], template:"<h1>hello,vivi</h1>", methods:{ bar:function(){ console.log("bar"); }, conflicting:function(){ console.log("来自组件本身"); } } }); var vm=new component(); vm.foo();//因为混合才有foo方法,所以打印foo vm.bar();//因为只有组件才有bar方法,所以打印bar vm.conflicting();//因为这个方法混合对象和组件都有,根据组件优先的原则,所以打印“来自组件本身”。 </script> </body>. </html>
调用的打印结果如下
第四,混合全局注册要慎用。会影响之后创建的所有vue实例。大多数情况下,只用于自定义选项。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> </div> <script src="js/vue.js"></script> <script> Vue.mixin({ created:function(){ var myOption=this.$options.myOption; if(myOption){ console.log(myOption); } } } ); var component=Vue.extend({ template:"<h1>hello,vivi!</h1>" }); new Vue({ el:"#app", components:{ "my-component":component }, myOption:"hello" }) </script> </body> </html>
4.动态组件
多个组件可以使用同一个挂载点,然后动态的在它们之间切换。
第一,动态组件的简单例子
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="radio" id="one" value="dev" v-model="currentView"/> <label for="one">开发</label> <br/> <input type="radio" id="two" value="painting" v-model="currentView"/> <label for="two">画画</label> <br/> <input type="radio" id="three" value="travel" v-model="currentView"/> <label for="three">旅游</label> <br/> <template id="dev"> <div>开发</div> </template> <template id="painting"> <div>画画</div> </template> <template id="travel"> <div>旅游</div> </template> <component :is="currentView"> <!--组件在vm.currentView变化时改变--> </component> </div> <script src="js/vue.js"></script> <script> var dev=Vue.extend({ template:"#dev", replace:"true" }); var painting=Vue.extend({ template:"#painting", replace:"true" }); var travel=Vue.extend({ template:"#travel", replace:true }); var vm=new Vue({ el:"#app", data:{ currentView:"dev" }, components:{ dev:dev, painting:painting, travel:travel } }) </script> </body> </html>
显示效果
第二,
keep-alive特性会让组件重复创建时通过缓存来获取,从而快速创建,当然是提高了视图的性能。
使用的语法如下:
<component :is="currentView" keep-alive> <!--组件在vm.currentView变化时改变--> </component>
从源码中可以窥得一二:
第三,activate钩子函数。
它是组件切入前执行。可用来控制组件切换的时长。activate钩子只作用于动态组件切换或者静态组件初始化渲染的过程。不用于使用实例方法手动插入的过程。
完整的代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="radio" id="one" value="dev" v-model="currentView"/> <label for="one">开发</label> <br/> <input type="radio" id="two" value="painting" v-model="currentView"/> <label for="two">画画</label> <br/> <input type="radio" id="three" value="travel" v-model="currentView"/> <label for="three">旅游</label> <br/> <template id="dev"> <div>开发</div> </template> <template id="painting"> <div>画画</div> </template> <template id="travel"> <div>旅游</div> </template> <component :is="currentView" transition="fade" transition-mode="out-in"> <!--改变的代码,先淡出再淡入--> </component> </div> <style> .fade-transition{ transition:opacity .3s ease; } .fade-enter,.fade-leave{ opacity:0; } </style> <script src="js/vue.js"></script> <script> var dev=Vue.extend({ template:"#dev", replace:"true", activate:function(done){ var self=this; console.log("在组件替换前打印"); setTimeout(function(){ console.log("计时器中的代码"); done(); },5000); //延时5秒 } }); var painting=Vue.extend({ template:"#painting", replace:"true" }); var travel=Vue.extend({ template:"#travel", replace:true }); var vm=new Vue({ el:"#app", data:{ currentView:"dev" }, components:{ dev:dev, painting:painting, travel:travel } }) </script> </body> </html>
可以看到在组件被插入前,它会执行activate函数,做了延时5秒的操作。
第四,transition-mode特性。用于指定两个动态组件之间如何过渡。
代码如下:
<component :is="currentView" transition="fade" transition-mode="out-in"> <!--改变的代码,先淡出再淡入--> </component>
<style> .fade-transition{ transition:opacity .3s ease; } .fade-enter,.fade-leave{ opacity:0; } </style>
完整的有渐进效果的DEMO:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <input type="radio" id="one" value="dev" v-model="currentView"/> <label for="one">开发</label> <br/> <input type="radio" id="two" value="painting" v-model="currentView"/> <label for="two">画画</label> <br/> <input type="radio" id="three" value="travel" v-model="currentView"/> <label for="three">旅游</label> <br/> <template id="dev"> <div>开发</div> </template> <template id="painting"> <div>画画</div> </template> <template id="travel"> <div>旅游</div> </template> <component :is="currentView" transition="fade" transition-mode="out-in"> <!--改变的代码,先淡出再淡入--> </component> </div> <style> .fade-transition{ transition:opacity .3s ease; } .fade-enter,.fade-leave{ opacity:0; } </style> <script src="js/vue.js"></script> <script> var dev=Vue.extend({ template:"#dev", replace:"true" }); var painting=Vue.extend({ template:"#painting", replace:"true" }); var travel=Vue.extend({ template:"#travel", replace:true }); var vm=new Vue({ el:"#app", data:{ currentView:"dev" }, components:{ dev:dev, painting:painting, travel:travel } }) </script> </body> </html>
5.生命周期
在vue.js中,实例化vue之前,它们以HTML的文本形式保存在文本编辑器中。当实例化后将经历,创建、编译、销毁3个主要阶段。
生命周期钩子:
(1)init:
在实例开始初始化时同步调用,此时数据观测、事件和watcher都尚未初始化。
(2)created:
在实例创建之后同步调用,此时实例已经结束解析选项,也就是数据绑定、计算属性、方法、watcher/事件回调已经建立。但是还没有开始DOM编译,$el还不存在。
(3)beforeCompile:
在编译开始前调用。
(4)compiled:
编译结束后调用,此时所有的指令已生效,因而数据的变化将触发DOM更新。但是不担保$el已插入文档。
(5)ready:
在编译结束和$el第一次插入文档之后调用,注意必须有vue插入(比如vm.$appendTo())才能触发ready钩子的。
(6)attached:
vm.$el插入DOM时调用。必须是由指令或者实例方法(比如$appendTo())插入,直接操作$vm.el不会触发钩子。
(7)detached
在vm.$el从DOM中删除时调用。必须是由指令或者实例方法删除,直接操作vm.$el不会触发这个钩子。
(8)beforeDestory:
在开始销毁实例时调用,此时实例仍然有功能。
(9)destroyed:
在实例销毁之后调用。
6.开发组件
使用.vue文件实现组件化。具体的代码执行请参看vue.js工具篇的webpack。
<template> <div class="header"> <h1> {{ title }} </h1> </div> </template> <script> export default { data: function () { return { title: ‘我是頭部‘ } } } </script> <style> .header { color: red; } </style>