- 模板:(template)模板聲明了數據和最終展現給用戶的DOM之間的映射關系。
- 初始數據:(data)一個組件的初始數據狀態。對於可復用的組件來說,通常是私有的狀態。
- 接收外部參數:(props)組件之間通過參數來進行數據的傳遞和共享。參數默認是單向綁定(由上至下),但也可以顯式聲明為雙向綁定。
- 方法:(methods)對數據的改動操作一般都在組件的方法內進行。可以通過v-on指令將用戶輸入事件和組件方法進行綁定。
- 聲明周期鈎子函數:(lifecycle hooks)一個組件會觸發多個生命周期鈎子函數,比如created、attached、destroyed等。在這些鈎子函數中,我們可以封裝一些自定義的邏輯。和傳統的MVC相比,這可以理解為Controller的邏輯被分散到了這些鈎子函數中。
一、注冊組件
- 全局組件
- 局部組件
1.1全局組件注冊:Vue.component('didi-component',DIDIComponent)
參數1('didi-component'):注冊組件的名稱,即在HTML中可以使用對應名稱的自定義標簽來添加組件:<didi-component></didi-component>,名稱除了使用中划線與html中添加自定義標簽一致以外,還可以使用小駝峰命名方式來定義名稱,同樣vue內部會自動匹配到中划線的html自定義標簽上,即‘didi-component’同等於‘didiComponent’,也有不規范的寫法直接自定義任意英文字符,不采用連接符(中划線)也不采用小駝峰命名,也是可以的,后面示例中會有出現。
參數2(DIDIComponent):注冊組件的鈎構造函數Function,也可以是Object。
1 //組件構造器構造組件: 2 var MyComponent = Vue.extent({ 3 //選項... 4 }) 5 //傳入選項對象注冊全局組件: 6 Vue.component('didi-component',{ 7 template:`<div>A custom component!</div>` 8 })
實例代碼(代碼折疊):

1 <div id="example"> 2 <didi-component></didi-component> 3 </div> 4 <script> 5 var DIDIComponent = Vue.extend({ 6 template:`<div>A custom component!</div>` 7 }) 8 //注冊 9 Vue.component('didi-component',DIDIComponent) 10 //創建vue根實例 11 new Vue({ 12 el:'#example' 13 }) 14 </script>
1.2局部組件注冊:也可以說是在vue實例上注冊組件。
1 <div id="example"> 2 <didi-component></didi-component> 3 </div> 4 <script> 5 //注冊局部組件 6 var DIDIComponent = Vue.extend({ 7 template:`<div>i am child!</div>` 8 }); 9 new Vue({ 10 el:'#example', 11 components:{ 12 didiComponent:DIDIComponent 13 } 14 }); 15 </script>
所謂局部組件就是不使用Vue.component()注冊組件,而是直接在vue實例上,通過component添加組件,上面的示例中將組件構造寫在實例外面,也可以直接寫在實例中:
1 new Vue({ 2 el:'#example', 3 components:{ 4 didiComponent:Vue.extend({ 5 template:`<div>i am child!</div>` 6 }) 7 } 8 });
注:現在的vue中構造組件可以不用寫Vue.extend()方法,而是可以直接寫成對象形式,后面的示例中將全部省略Vue.extend()。
全局組件與局部組件除了全局組件通過Vue.component()方法注冊,而局部組件直接通過component添加到vue實例對象上以外。全局組件注冊會始終以構造的形式被緩存,而局部組件不被使用時,不會被構造緩存,而是在vue實例需要時才被構造。雖然說將組件構造緩存在內存中可以提高代碼執行效率,但是另一方面是消耗大量的內存資源。
除了上面的單個構造注冊,也可以直接在模板中使用引用其他局部組件:
1 <div id="example"> 2 <didi-component></didi-component> 3 </div> 4 <script> 5 var Child = { 6 template:`<div>i am child!</div>`, 7 replace:false 8 } 9 var Parent = { 10 template:`<div>//這個包裝元素現在不能使用template標簽了,以前可以 11 <p>i am parent</p> 12 <br/> 13 <child-component></child-component> 14 </div>`, 15 components:{ 16 'childComponent':Child//在局部組件模板中直接引用其他局部組件 17 } 18 } 19 new Vue({ 20 el:'#example', 21 components:{ 22 didiComponent:Parent 23 } 24 }) 25 </script>
二、數據傳遞
- props
- 組件通信
- slot
2.1通過props實現數據傳輸:
通過v-bind在組件自定義標簽上添加HTML特性建立數據傳遞通道;
在組件字段props上綁定自定義標簽上傳遞過來的數據;
1 <div id="example"> 2 <!--將vue實例中的數據resultsList綁定到自定義特性list上--> 3 <didi-component :list="resultsList"></didi-component> 4 </div> 5 <script> 6 //綁定數據 7 var Child = { 8 props:['list'],//通過自定義特性list獲取父組件上的resultsList 9 template: `<div> 10 <p> 11 <span>姓名</span> 12 <span>語文</span> 13 <span>數學</span> 14 <span>英語</span> 15 </p> 16 <ul> 17 <li v-for="item in list" :key="item.name"> 18 <span>{{item.name}}</span> 19 <span>{{item.results.language}}</span> 20 <span>{{item.results.math}}</span> 21 <span>{{item.results.english}}</span> 22 </li> 23 </ul> 24 </div>` 25 } 26 var vm = new Vue({ 27 el:'#example', 28 components:{ 29 didiComponent:Child 30 }, 31 data:{ 32 resultsList:[ 33 { 34 name:"張三", 35 results:{language:89,math:95,english:90} 36 }, 37 { 38 name:"李四", 39 results:{language:92,math:76,english:80} 40 }, 41 { 42 name:"王五", 43 results:{language:72,math:86,english:98} 44 } 45 ] 46 } 47 }); 48 </script>
通過上面的示例可以了解到子組件獲取父組件上的數據過程,這你一定會有一個疑問,props這里這有什么作用,為什么需要props這個字段?
a.每個組件都有自己獨立的實例模型來管理自己身的屬性,通過props獲取到父組件中自身需要的數據,並使用自身屬性緩存數據引用可以提高程序執行效率,如果將父組件上的數據全盤接收過來,對於子組件自身來說會有大量多余的數據,反而降低程序執行效率。
b.通過props數據校驗,保證獲取到正確無誤的數據,提高程序的可靠性。
2.2props接收數據的方式及校驗處理:
1 //采用數組形式接收父組件通過自定義特性傳遞過來的數據 2 props:['data1','data2','data3'...]//數組接收方式只負責數據接收,並不對數據校驗處理 3 4 //采用對象形式接收父組件通過自定義特性傳遞過來的數據 5 props:{ 6 data1:{ 7 type:Array,//校驗接收數據類型為Array,type的值還可以是數組,添加數據可符合多種數據類型的校驗 8 default:[{name:"我是默認參數",...}],//當data1沒有接收到數據時,組件就會使用默認數據渲染 9 required:true,//校驗必須接收到數據,不然即使在有默認數據的情況下也會在控制台打印出報錯提示 10 validator(value){//校驗數據具體信息,參數value接收到的是傳遞過來的數據,如果方法返回false則表示數據不符合條件,控制台打印出報錯提示 11 return value.length < 1; 12 } 13 }
采用數據校驗只能保證在數據有誤時在控制台打印出相對准確的錯誤提示,並不會阻止數據渲染,也不會阻塞后面的代碼執行。
2.3棧內存傳值與單向數據流
1 <div id="example"> 2 父級對象傳值:<input type="text" v-model="info.name"/> 3 父級對象屬性傳值:<input type="text" v-model="obj.name"> 4 父級字符串傳值:<input type="text" v-model="name"> 5 <child v-bind:msg1.sync="info" v-bind:msg2="obj.name" v-bind:msg3="name"></child> 6 </div> 7 <script> 8 new Vue({ 9 el:'#example', 10 data:{ 11 info:{ 12 name:'順風車' 13 }, 14 obj:{ 15 name:"專車" 16 }, 17 name:"快車" 18 }, 19 components:{ 20 'child':{ 21 props:['msg1','msg2','msg3'], 22 template:` <div> 23 接收對象傳值:<input type="text" v-model="msg1.name"/> 24 接收對象屬性傳值:<input type="text" v-model="msg2"> 25 接收字符串傳值:<input type="text" v-model="msg3"> 26 </div>` 27 } 28 } 29 }) 30 </script>
示例效果:
通過示例可以看到vue父子組件傳值,采用的是父級向子級的單向數據流,當父級數據發生變化時,子級數據會跟着變化。但同時又因為傳遞值的方式采用的是棧內存賦值方式,如果父級傳遞給子級的是引用值類型數據,在子級中改變數據也會引發父級數據的更改。
注:在權威指南中有說可以通過給數據傳遞添加修飾符once和sync來控制數據的但雙向綁定,在2.x中測試無效,但添加這兩個修飾符不報錯。關於個問題我想應該是版本更新優化的,但不能確定,畢竟我沒有使用過之前的版本,也沒有閱讀之前版本的手冊和源碼,這個一點暫時保留意見。
由於vue父子組件傳值采用了棧內存傳值,就沒有辦法保證數據的單向傳遞,解決這個問題的辦法很簡單,在子組件中通過克隆該數據用自身的數據引用保存下來就OK了,但要注意在子組件中聲明data需要使用function類型。
注:即便傳遞的數據是原始值類型的數據,也不要直接使用接收的數據,雖然程序能正常運行,但是vue在控制台對直接使用原始值的行為報警告,所以正確的數據接收方式是通過props接收后在子組件自身的數據上再創建一個數據副本來使用。
data(){ return{ //自身數據引用名稱:接收父級傳遞來的引用數據的克隆數據 } }

1 <div id="example"> 2 父級對象傳值:<input type="text" v-model="info.name"/> 3 父級對象屬性傳值:<input type="text" v-model="obj.name"/> 4 父級字符串傳值:<input type="text" v-model="name"/> 5 <chiled-component v-bind:msg1="info" v-bind:msg2="obj.name" v-bind:msg3="name"></chiled-component> 6 </div> 7 <script> 8 new Vue({ 9 el:"#example", 10 data:{ 11 info:{ 12 name:"順風車" 13 }, 14 obj:{ 15 name:"專車" 16 }, 17 name:"快車" 18 }, 19 components:{ 20 chiledComponent:Vue.extend({ 21 props:["msg1","msg2","msg3"], 22 data(){ 23 return { 24 info:{name:this.msg1.name}, 25 obj:{name:this.msg2}, 26 name:this.msg3 27 } 28 }, 29 template:`<div> 30 接收對象傳值:<input type="text" v-model="info.name"/> 31 接收對象屬性傳值:<input type="text" v-model="obj.name"/> 32 接收字符串傳值:<input type="text" v-model="name"/> 33 </div>` 34 }) 35 } 36 }) 37 </script>
測試改良后的代碼會發現子組件上的info數據發生變化,不再會觸發父級數據的渲染,這有效的隔離了子組件對數據的父級數據的影響,更凸顯了面向對象的思想,讓行為粒度更小,這是好事情,不信你可以考慮一種情況,如果這個模塊從最頂層到最底層的組件有很多個,而且還與別的模塊共享數據,如果一個組件發生變化就觸發所有數據渲染,這是一個多么糟糕的性能問題甚至還會產生很多關聯的問題,但可能有的人會說如果本來就需要數據狀態共享,即便是這樣也不應該采用這種暴力的方法,而是通過切換每一個視圖來按需觸發數據狀態共享。
三、組件通信
在上一節介紹了父子組件數據傳遞,我們知道vue采用的是單向數據流,這就意味着當子組件需要向父組件傳值時就得需要其他手段,畢竟在更多時候子組件很可能會有復用性情況,所以子組件也不能直接去更改父組件的數據,但總會有很多需求需要我們將子組件的計算結果傳遞給父組件。
Vue通過在父級作用域上定義事件,然后再由子組件使用$emit來實現事件派送,當派送到對應事件作用域時,使用派送過來的參數(數據)觸發事件回調函數,這就是組件通信。通過組件通信可以解決子組件向父組件傳值,示例:

1 <div id="example"> 2 <!-- 通過transmitdata接收來自子組件的傳輸指令,然后觸發父組件的addComData方法 --> 3 <comment-complate @transmitdata="addComData"></comment-complate> 4 <ul> 5 <li v-for="item in comList" :key="item.id"> 6 <div> 7 <span class="userName">{{item.userName}}</span> 8 <span class="comDate">{{item.date}}</span> 9 </div> 10 <div>{{item.val}}<div> 11 </li> 12 </ul> 13 </div> 14 <script> 15 var comment = { 16 template: `<div> 17 <textarea v-model="comVal"></textarea> 18 <p><button @click="submitData">提交</button></p> 19 </div>`, 20 data(){ 21 return { 22 userId:1001, 23 userName:"他鄉踏雪", 24 comVal:"" 25 } 26 }, 27 methods:{ 28 submitData(){ 29 console.log(this.comVal + 'a'); 30 var comDate = new Date(); 31 let comData = { 32 id:Number(''+this.userId + comDate.getTime()), 33 userName:this.userName, 34 date:comDate.toLocaleDateString() + ' ' + comDate.toLocaleTimeString(), 35 val:this.comVal 36 } 37 this.$emit('transmitdata',comData);//通過$emit監聽提交留言的點擊事件,被觸發后將數據傳遞給自定義方法transmitdata 38 } 39 } 40 } 41 var vm = new Vue({ 42 el:'#example', 43 components:{ 44 commentComplate:comment 45 }, 46 data:{ 47 comList:[ 48 { 49 userName:"南都谷主", 50 id:1001, 51 date:'2019/7/8 上午00:32:55', 52 val:'2017年,在長春園東南隅的如園遺址,工作人員在進行' 53 } 54 ] 55 }, 56 methods:{ 57 addComData(data){ 58 //transmitdata自定義事件接收到數據傳遞請求后,將傳遞過來的數據交給addComData處理 59 this.comList.unshift(data); 60 } 61 } 62 }); 63 </script>
如果用來接收數據傳輸數據的事件是一個原生的DOM事件,就不必使用$emit()來監聽事件觸發,只需要在原生的事件聲明后添加一個‘.navite’后綴就可以自動實現監聽事件觸發,原生事件本身就是由瀏覽器自身監聽,所以不必要多余的操作。如:@click.navite="父組件的事件監聽方法"。但是要注意的是這種事件不具備組件通信能力,因為實質上這個方法是父級組件定義在內部DOM上的一個原生事件,不能被子組件的$emit偵聽到,也就是說在子組件內不是無法觸發這個事件的。
<comment-complate @click="addComData"></comment-complate>
四、插槽與動態組件
4.1插槽:<slot></slot>
有時候需要在父級組件中給子組件添加一些節點內容,這時候就可以在使用slot來實現,並且還可以在子組件中使用具名插槽,來實現指定位置插入節點。示例:
1 <div id="app"> 2 <child> 3 <span>{{titleName}}:</span><!--插入插槽--> 4 </child> 5 </div> 6 <script> 7 var vm = new Vue({ 8 el:'#app', 9 components:{ 10 child:{ 11 template:` <div> 12 <slot></slot><!--定義插槽--> 13 <input type="text" /> 14 </div>` 15 } 16 }, 17 data:{ 18 titleName:'郵箱' 19 } 20 }); 21 </script>
通過上面的插槽功能就可以動態的切換輸入框的標題,在一些需求中有多種內容輸入方式,就不需要去定義多個組件來實現,只需要在父級組件來切換輸入標題,子組件只負責輸入操作,就可以實現在同一個組件上實現多個輸入場景。
有了多種內容輸入場景就必然需要多種輸入提示,這種輸入提示必定是需要與輸入標題相配合,數據必然是同樣與標題內容處於父級組件上,就需要多個插槽來實現,這時候就需要用到具名插槽:
1 <div id="app"> 2 <child> 3 <span slot="title">{{titleName}}:</span><!--插入標題插槽--> 4 <span slot="hint">{{hint}}</span> 5 </child> 6 </div> 7 <script> 8 var vm = new Vue({ 9 el:'#app', 10 components:{ 11 child:{ 12 template:` <div> 13 <slot name="title"></slot><!--定義標簽具名插槽--> 14 <input type="text" /> 15 <slot name="hint"></slot><!--定義提示具名插槽--> 16 </div>` 17 } 18 }, 19 data:{ 20 titleName:'郵箱', 21 hint:"請輸入正確的郵箱地址" 22 } 23 }); 24 </script>
通過具名插槽可以將父級插入的節點插入到指定的地方,當然這時候你會說可以直接在子組件上來實現這些數據切換,這是肯定可行的,但是使用子組件實現就必然會涉及到父子組件傳值。有可能你也會想到這樣的功能也可以使用一個父組件就可以實現,為什么還要使用子組件呢?這也當然是可以的,但是這同樣涉及到了另一個問題,如果是復雜一點的需求呢?所以,沒有絕對的設計優勢,這要看具體需求,如果是在比較復雜的需求中就可以通過插槽的方式將輸入操作與業務邏輯通過組件層級分離。
插槽除了上面的應用,在接下來的動態組件中也會有很大作用。
4.2動態組件:
所謂動態組件就是在父組件中定義一個子組件,可以通過數據來切換實現引入不同的子組件,這時你一定會想這不就可以通過v-if和v-else來實現嗎?注意,v-if和v-else來配合實現只能實現兩個組件切換,而且需要在父組件中引入兩個子組件,這與動態組件只需要引入一個子組件,並且可以切換多個子組件的功能來比較相差甚遠。
語法:<component is=“綁定指定的子組件”></component>
示例:
1 <div id="app"> 2 <button @click="chanegCmp">切換組件</button> 3 <component :is="cmpType"></component> 4 </div> 5 <script> 6 const cmpOne = { 7 template:` <div> 8 <span>組件1:</span> 9 <input type="text" /> 10 </div>` 11 } 12 const cmpTwo = { 13 template:` <div> 14 <span>組件2:</span> 15 <input type="text" /> 16 </div>` 17 } 18 const cmpThree = { 19 template:` <div> 20 <span>組件3:</span> 21 <input type="text" /> 22 </div>` 23 } 24 var vm = new Vue({ 25 el:'#app', 26 data:{ 27 cmpList:["cmpOne","cmpTwo","cmpThree"], 28 cmpType:"cmpOne", 29 cmpPresent:0 30 }, 31 components:{ 32 cmpOne:cmpOne, 33 cmpTwo:cmpTwo, 34 cmpThree:cmpThree 35 }, 36 methods:{ 37 chanegCmp(){ 38 console.log(this.cmpPresent + ' - ' + this.cmpList.length) 39 if( (this.cmpPresent + 1) === this.cmpList.length){ 40 this.cmpPresent = 0; 41 this.cmpType = this.cmpList[this.cmpPresent]; 42 }else{ 43 this.cmpPresent ++; 44 this.cmpType = this.cmpList[this.cmpPresent]; 45 } 46 } 47 } 48 }); 49 </script>
示例實現效果:
雖然可以通過動態組件切換組件,但是上面的示例可能還並不能是我們想要的,因為在切換組件時並不能保留組件的狀態,比如我們在第一次切換組件時,輸入文本后第二次切換回到這個組件時,輸入的文本並不能不被保留,也就是說重新切換回來的組件是一個全新的渲染組件,並不上次的原組件,
針對這種需求vue給我提供了一個標簽<keep-alive>,使用這個標簽包裹component標簽就可以實現保留子組件的狀態了,所以示例中的代碼可以這樣修改:
1 <keep-alive> 2 <component :is="cmpType"></component> 3 </keep-alive>
4.3作用域插槽
在前面的4.1中介紹了插槽,它可以在父級來定義相對靈活的子組件,在前面的示例中只是介紹了父級在子級指定為節點位置插入一些元素節點,這時我想應該又引發了我們的另一個思考,既然插槽可以在父級作用定義子級特定位置的節點,那可不可以實現父級作用域定義子級特定的數據模板呢?也就是子級不僅可以提供一個占位,還可以提供數據引用來實現在父級組件中定義元素的節點和數據渲染。這個光是用文字來描述會有點繞,先來看一個小示例:
1 <div id="app"> 2 <child :userdata="user"><!--獲取父組件的數據user,並定義新的引用名稱userdata--> 3 <template slot-scope="list"><!--數據模板上通過slot-scope特性接收插槽傳來的數據,並定義索引名list--> 4 <span>{{list.username}} -- {{list.userCom}}</span><!--通過模板接收到的數據索引取出數據並渲染到頁面--> 5 </template> 6 </child> 7 </div> 8 <script> 9 const child = { 10 props:["userdata"],//接收父組件的數據userdata, 11 data(){ 12 return{ 13 comData:"child" 14 } 15 }, 16 template:` <div> 17 <!--通過插槽將接收到父組件的數據和子組件自身的數據傳給模板--> 18 <!--傳遞數據的方式就是將數據以特性的方式傳入slot標簽--> 19 <slot 20 :username="userdata.name" 21 :userCom="comData"></slot> 22 <input type="text"/> 23 </div>` 24 } 25 var vm = new Vue({ 26 el:'#app', 27 data:{ 28 user:{ 29 name:"他鄉踏雪", 30 } 31 }, 32 components:{ 33 child:child 34 } 35 }); 36 </script>
這里的一個關鍵就是通過<slot>插槽標簽的特性鍵數據傳出,然后通過模板特性slot-scope接收這些數據,再來提供一個示例來進一步了解作用域插槽:

1 <div id="app"> 2 <cmp-two :list="list"> 3 <template slot-scope="list"> 4 <li>{{list.item}} - {{list.index}}</li> 5 </template> 6 </cmp-two> 7 <cmp-two :list="list"> 8 <template slot-scope="list"> 9 <li>{{list.index}} - {{list.item}}</li> 10 </template> 11 </cmp-two> 12 </div> 13 <script> 14 const cmpTwo = { 15 props:['list'], 16 template:`<div> 17 組件2:<input tyle="text"> 18 <ul> 19 <slot v-for="(item,index) in list" 20 :item="item" 21 :index="index"> 22 </slot> 23 </ul> 24 </div>` 25 } 26 var vm = new Vue({ 27 el:'#app', 28 components:{ 29 cmpTwo 30 }, 31 data:{ 32 list:[1,2,3,4,5] 33 } 34 }); 35 </script>