本文的Demo和源代碼已放到GitHub,如果您覺得本篇內容不錯,請點個贊,或在GitHub上加個星星!
https://github.com/zwl-jasmine95/Vue_test
以下所有知識都是基於vue.js 2.0版本
組件 (Component) 是 Vue.js 最強大的功能之一。組件可以擴展 HTML 元素,封裝可重用的代碼。在較高層面上,組件是自定義元素,Vue.js 的編譯器為它添加特殊功能。在有些情況下,組件也可以是原生 HTML 元素的形式,以 is
特性擴展。
一、組件的創建與注冊
1、全局組件
組件的使用有兩個步驟:注冊組件 和 使用組件。
(1)要注冊一個全局組件,可以使用 :
Vue.component(tagName, options)
例如:
Vue.component('my-component', { // 選項 })
(2)創建根實例:
//創建根實例 var vm = new Vue({ el:'#component-demo' });
案例:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>全局組件(component)的基本使用</title> 6 </head> 7 <body> 8 <div id="component-demo"> 9 <!-- 2. #component-demo是Vue實例掛載的元素,應該在掛載元素范圍內使用組件--> 10 <hello-component></hello-component> 11 </div> 12 13 <script type="text/javascript" src="../lib/js/vue.js"></script> 14 <script type="text/javascript"> 15 16 //1.全局組件注冊,並指定組件的標簽,組件的HTML標簽為<hello-component> 17 Vue.component('hello-component',{ 18 template:'<h1>hello component!</h1>' 19 }); 20 21 //創建根實例 22 var vm = new Vue({ 23 el:'#component-demo' 24 }); 25 26 </script> 27 </body> 28 </html>
注意事項:
- 對於自定義標簽名,Vue.js 不強制要求遵循 W3C 規則 (小寫,並且包含一個短杠),盡管遵循這個規則比較好。
- 注冊組件必須發生在根實例初始化前。
- 如果自定義標簽名使用的是駝峰式命名,在使用的時候仍然要在大寫字母處加上短杠,並將大寫字母改為小寫。例如:命名為‘helloWorldComponent’,在使用時變為<hello-world-component></hello-world-component>
2、局部組件
調用Vue.component()
注冊組件時,組件的注冊是全局的,這意味着該組件可以在任意Vue示例下使用。
如果不需要全局注冊,或者是讓組件使用在其它組件內,可以用選項對象的components屬性實現局部注冊。
1 <div id="component-demo"> 2 <local-component></local-component> 3 </div> 4 <script type="text/javascript"> 5 6 var child = { 7 template:'<h1>局部組件的基本使用!</h1>' 8 }; 9 10 var vm = new Vue({ 11 el:'#component-demo', 12 components:{ 13 'local-component':child 14 } 15 }); 16 </script>
由於local-component組件是注冊在#component-demo元素對應的Vue實例下的,所以它不能在其它Vue實例下使用。如果你這樣做了,瀏覽器會提示一個錯誤:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>局部組件</title> 6 </head> 7 <body> 8 9 <div id="component-demo"> 10 <local-component></local-component> 11 </div> 12 13 <div id="component-demo2"> 14 <!--這里會報錯,因為local-component是#component-demo的局部組件,不能在其他地方使用--> 15 <local-component></local-component> 16 </div> 17 18 <script type="text/javascript" src="../lib/js/vue.js"></script> 19 <script type="text/javascript"> 20 21 var child = { 22 template:'<h1>局部組件的基本使用!</h1>' 23 }; 24 25 var vm = new Vue({ 26 el:'#component-demo', 27 components:{ 28 'local-component':child 29 } 30 }); 31 32 var vm2 = new Vue({ 33 el:'#component-demo2' 34 }); 35 36 37 </script> 38 </body> 39 </html>
組件意味着協同工作,通常父子組件會是這樣的關系:組件 A 在它的模版中使用了組件 B。它們之間必然需要相互通信:父組件要給子組件傳遞數據,子組件需要將它內部發生的事情告知給父組件。然而,在一個良好定義的接口中盡可能將父子組件解耦是很重要的。這保證了每個組件可以在相對隔離的環境中書寫和理解,也大幅提高了組件的可維護性和可重用性。
在 Vue 中,父子組件的關系可以總結為 props down, events up。父組件通過 props 向下傳遞數據給子組件,子組件通過 events 給父組件發送消息。
二、組件的DOM模板解析說明
當使用 DOM 作為模版時 (例如,將 el
選項掛載到一個已存在的元素上), 你會受到 HTML 的一些限制,因為 Vue 只有在瀏覽器解析和標准化 HTML 后才能獲取模版內容。尤其像這些元素 <ul>
,<ol>
,<table>
,<select>
限制了能被它包裹的元素,而一些像 <option>
這樣的元素只能出現在某些其它元素內部。
在自定義組件中使用這些受限制的元素時會導致一些問題,例如:
<table> <my-row>...</my-row> </table>
自定義組件 <my-row>
被認為是無效的內容,因此在渲染的時候會導致錯誤。變通的方案是使用特殊的 is
屬性:
<table> <tr is="my-row"></tr> </table>
應當注意,如果您使用來自以下來源之一的字符串模板,這些限制將不適用:
<script type="text/x-template">
- JavaScript 內聯模版字符串
.vue
組件因此,有必要的話請使用字符串模版。
三、組件字符串模板
上述組件注冊方法在template選項中拼接HTML元素比較麻煩,這也導致了HTML和JavaScript的高耦合性。慶幸的是,Vue.js提供了兩種方式將定義在JavaScript中的HTML模板分離出來。
1、使用 script 標簽
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>使用script標簽</title> 6 </head> 7 <body> 8 <div id="demo"> 9 <my-component></my-component> 10 </div> 11 12 <script type="text/x-template" id="myComponent"> 13 <h1>組件使用之使用script標簽</h1> 14 </script> 15 16 <script type="text/javascript" src="../lib/js/vue.js"></script> 17 <script type="text/javascript"> 18 Vue.component('my-component',{ 19 template:'#myComponent' 20 }); 21 22 var vm = new Vue({ 23 el:'#demo' 24 }); 25 </script> 26 </body> 27 </html>
template選項現在不再是HTML元素,而是一個id,Vue.js根據這個id查找對應的元素,然后將這個元素內的HTML作為模板進行編譯。
注意:使用<script>標簽時,type指定為text/x-template,意在告訴瀏覽器這不是一段js腳本,瀏覽器在解析HTML文檔時會忽略<script>標簽內定義的內容。
2、使用 template 標簽
如果使用<template>
標簽,則不需要指定type屬性。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>使用template標簽</title> 6 </head> 7 <body> 8 <div id="demo"> 9 <my-component></my-component> 10 </div> 11 12 <template id="myComponent"> 13 <h1>組件使用之使用template標簽</h1> 14 </template> 15 16 <script type="text/javascript" src="../lib/js/vue.js"></script> 17 <script type="text/javascript"> 18 Vue.component('my-component',{ 19 template:'#myComponent' 20 }); 21 22 var vm = new Vue({ 23 el:'#demo' 24 }); 25 </script> 26 </body> 27 </html>
建議使用<script>或<template>標簽來定義組件的HTML模板。這使得HTML代碼和JavaScript代碼是分離的,便於閱讀和維護。
四、組件data選項的限制
通過 Vue 構造器傳入的各種選項大多數都可以在組件里用。data
是一個例外,它必須是函數。
如果這樣寫:
Vue.component('hello-component',{ template:'<h1>{{message}}</h1>', data:{ message:1 } });
會報錯:
所以應該改為:
Vue.component('my-component',{ template:'<h1>{{message}}</h1>', data:function () { return {message : 'data必須為函數'}; } });
五、props—父組件向子組件傳遞信息
組件實例的作用域是孤立的。這意味着不能 (也不應該) 在子組件的模板內直接引用父組件的數據。要讓子組件使用父組件的數據,我們需要通過子組件的 props 選項。
1、靜態數據
1 <div id="demo"> 2 <child-component message="hello props!"></child-component> 3 </div> 4 5 <template id="myComponent"> 6 <h1>{{message}}</h1> 7 </template> 8 9 <script type="text/javascript" src="../lib/js/vue.js"></script> 10 <script type="text/javascript"> 11 var child = { 12 // 聲明 props 13 props: ['message'], 14 // 就像 data 一樣,prop 可以用在模板內 15 // 同樣也可以在 vm 實例中像“this.message”這樣使用 16 template:'#myComponent' 17 }; 18 19 var vm = new Vue({ 20 el:'#demo', 21 components:{ 22 'child-component':child 23 } 24 }); 25 </script>
HTML 特性是不區分大小寫的。所以,當使用的不是字符串模版,camelCased (駝峰式) 命名的 prop 需要轉換為相對應的 kebab-case (短橫線隔開式) 命名
2、動態數據
在模板中,要動態地綁定父組件的數據到子模板的 props,與綁定到任何普通的HTML特性相類似,就是用 v-bind
。每當父組件的數據變化時,該變化也會傳導給子組件。
1 <body> 2 <div id="demo"> 3 父組件信息:<input type="text" v-model="parentMessage"> 4 <child-component v-bind:message="parentMessage"></child-component> 5 </div> 6 7 <template id="myComponent"> 8 <h1>子組件獲取的信息為:{{message}}</h1> 9 </template> 10 11 <script type="text/javascript" src="../lib/js/vue.js"></script> 12 <script type="text/javascript"> 13 14 var child = { 15 props: ['message'], 16 template:'#myComponent' 17 }; 18 19 var vm = new Vue({ 20 el:'#demo', 21 data:{ 22 parentMessage:'這里是父組件信息!' 23 }, 24 components:{ 25 'child-component':child 26 } 27 }); 28 </script> 29 </body>
初學者常犯的一個錯誤是使用字面量語法傳遞數值:
<!-- 傳遞了一個字符串 "1" --> <comp some-prop="1"></comp>因為它是一個字面 prop,它的值是字符串
"1"
而不是 number。如果想傳遞一個實際的 number,需要使用v-bind
,從而讓它的值被當作 JavaScript 表達式計算:<!-- 傳遞實際的 number --> <comp v-bind:some-prop="1"></comp>
3、props的綁定類型—單項綁定
prop 是單向綁定的:當父組件的屬性變化時,將傳導給子組件,但是不會反過來。這是為了防止子組件無意修改了父組件的狀態。
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>props單項數據綁定</title> 6 <link rel="stylesheet" href="../lib/css/bootstrap.min.css"> 7 </head> 8 <body> 9 <div id="demo"> 10 <table class="table table-striped"> 11 <tr> 12 <td colspan="3">父組件數據</td> 13 </tr> 14 <tr> 15 <td>姓名</td> 16 <td>{{parentName}}</td> 17 <td><input type="text" v-model="parentName"/></td> 18 </tr> 19 <tr> 20 <td>年齡</td> 21 <td>{{parentAge}}</td> 22 <td><input type="text" v-model="parentAge"/></td> 23 </tr> 24 </table> 25 26 <!--注意這里camelCased (駝峰式) 命名的 prop 需要轉換為相對應的 kebab-case (短橫線隔開式) 命名--> 27 <child-component v-bind:child-name="parentName" v-bind:child-age="parentAge"></child-component> 28 29 </div> 30 31 <template id="myComponent"> 32 <table class="table table-striped"> 33 <tr> 34 <td colspan="3">子組件數據</td> 35 </tr> 36 <tr> 37 <td>姓名</td> 38 <td>{{childName}}</td> 39 </tr> 40 <tr> 41 <td>年齡</td> 42 <td>{{childAge}}</td> 43 </tr> 44 </table> 45 </template> 46 47 <script type="text/javascript" src="../lib/js/vue.js"></script> 48 <script type="text/javascript"> 49 50 var child = { 51 template:'#myComponent', 52 props: ['childName','childAge'] 53 54 }; 55 56 var vm = new Vue({ 57 el:'#demo', 58 data:{ 59 parentName:'*茉莉花開*', 60 parentAge:22 61 }, 62 components:{ 63 'child-component':child 64 } 65 }); 66 </script> 67 </body> 68 </html>
父組件的數據更改時,子組件的數據也跟着修改:
每次父組件更新時,子組件的所有 prop 都會更新為最新值。這意味着你不應該在子組件內部改變 prop。如果你這么做了,Vue 會在控制台給出警告。
為什么我們會有修改 prop 中數據的沖動呢?通常是這兩種原因:
-
prop 作為初始值傳入后,子組件想把它當作局部數據來用;
-
prop 作為初始值傳入,由子組件處理成其它數據輸出。
對這兩種原因,正確的應對方式是:
-
定義一個局部變量,並用 prop 的值初始化它:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
-
定義一個計算屬性,處理 prop 的值並返回。
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
針對上述demo代碼,我們可以稍作修改,將子組件從父組件獲取的姓名變為新的姓名:
我們可以看到效果:
!!!在這里,父組件的姓名改變時,子組件的姓名不會跟着改變。
注意:
1.在 JavaScript 中對象和數組是引用類型,指向同一個內存空間,如果 prop 是一個對象或數組,在子組件內部改變它會影響父組件的狀態。
2.Vue 2.x相比較Vue 1.x而言,升級變化除了實現了Virtual-Dom以外,給使用者最大不適就是移除的組件的
props
的雙向綁定功能。以往在Vue1.x中利用props
的twoWay
和.sync
綁定修飾符就可以實現props的雙向綁定功能,但是在Vue2中徹底廢棄了此功能,如果需要雙向綁定需要自己來實現。
4、props驗證
我們可以為組件的 props 指定驗證規格。如果傳入的數據不符合規格,Vue 會發出警告。當組件給其他人使用時,這很有用。
要指定驗證規格,需要用 對象的形式,而不能用字符串數組:
Vue.component('example', { props: { // 基礎類型檢測 (`null` 意思是任何類型都可以) propA: Number, // 多種類型 propB: [String, Number], // 必傳且是字符串 propC: { type: String, required: true }, // 數字,有默認值 propD: { type: Number, default: 100 }, // 數組/對象的默認值應當由一個工廠函數返回 propE: { type: Object, default: function () { return { message: 'hello' } } }, // 自定義驗證函數 propF: { validator: function (value) { return value > 10 } } } })
type
可以是下面原生構造器:
- String
- Number
- Boolean
- Function
- Object
- Array
- Symbol
type
也可以是一個自定義構造器函數,使用instanceof
檢測。當 prop 驗證失敗,Vue 會在拋出警告 (如果使用的是開發版本)。注意 props 會在組件實例創建之前進行校驗,所以在
default
或validator
函數里,諸如data
、computed
或methods
等實例屬性還無法使用。
實例:將第三小節中的demo代碼修改,給傳遞的數據姓名和年齡加上驗證,姓名必須為字符串類型,年齡必須為數字且不為空:
在年齡那輸入“m”,可以看見控制台報錯了。因為傳遞的數據是字符串類型,而年齡props驗證要求的是數字。
六、組件自定義事件
父組件是使用 props 傳遞數據給子組件,但子組件怎么跟父組件通信呢?這個時候 Vue 的自定義事件系統就派得上用場了。
1、使用 v-on 綁定自定義事件
每個 Vue 實例都實現了事件接口 (Events interface),即:
- 使用
$on(eventName)
監聽事件 - 使用
$emit(eventName)
觸發事件
另外,父組件可以在使用子組件的地方直接用 v-on
來監聽子組件觸發的事件。不能用 $on
偵聽子組件拋出的事件,而必須在模板里直接用 v-on
綁定。

1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>v-on綁定自定義事件</title> 6 <script type="text/javascript" src="../lib/js/vue.js"></script> 7 </head> 8 <body> 9 <div id="counter-event-example"> 10 <p>{{ total }}</p> 11 <button-counter v-on:add="incrementTotal"></button-counter> 12 </div> 13 14 <script type="text/javascript"> 15 16 Vue.component('button-counter', { 17 template: '<button v-on:click="incrementCounter">{{ counter }}</button>', 18 data: function () { 19 return { 20 counter: 0 21 } 22 }, 23 methods: { 24 incrementCounter: function () { 25 this.counter += 1; 26 this.$emit('add'); //向父組件報告,自己發生了‘add’事件 27 } 28 } 29 }); 30 new Vue({ 31 el: '#counter-event-example', 32 data: { 33 total: 0 34 }, 35 methods: { 36 incrementTotal: function () { 37 this.total += 1 38 } 39 } 40 }) 41 </script> 42 </body> 43 </html>
分析:
第一步:在子組件里面把點擊事件(click)綁定給了函數 incrementCounter(即圖片里面的步驟1),這里容易理解,即點擊了子組件的按鈕將會觸發位於子組件的 incrementCounter函數。
第二步:在觸發 incrementCounter 函數的時候,子組件的數字在原來值的基礎上加1,並且表示向父組件報告自己觸發了 add 事件(至於發生的事件叫什么名字,可以隨意取名,只要在父組件中綁定時名稱一致即可。)。
第三步: 在子組件觸發add事件的時候,父組件調用 incrementTotal 函數來響應子組件。
這時我們回想步驟2,在子組件我們已經使用emit來進行通知,所以,這樣就形成父子組件間的相互呼應傳遞信息,其實在開發的過程中父子組件通訊也都是使用這樣的方法,父組件傳遞信息給子組件的時候會通過props參數,通常不會直接在子組件中修改父組件傳遞下來的信息,而且通過這樣的一個鈎子去通知父組件對某些參數進行改變。
第四步:定義父組件的 incrementTotal 函數,數值在原基礎上加1。
2、給組件綁定原生事件
有時候,你可能想在某個組件的根元素上監聽一個原生事件。可以使用 .native
修飾 v-on
。例如:
<my-component v-on:click.native="doTheThing"></my-component>
3、使用自定義事件的表單輸入組件
自定義事件可以用來創建自定義的表單輸入組件,使用 v-model
來進行數據雙向綁定。看看這個:
<input v-model="something">
這不過是以下示例的語法糖:
<input v-bind:value="something" v-on:input="something = $event.target.value">
所以在組件中使用時,它相當於下面的簡寫:
<custom-input v-bind:value="something" v-on:input="something = arguments[0]"></custom-input>
接受一個 value
屬性所以要讓組件的 v-model
生效,它應該 (在 2.2.0+ 這是可配置的):
- 接受一個
value
屬性
- 在有新的值時觸發
input
事件
demo:
1 <div id="demo"> 2 <currency-input v-model="price"></currency-input> 3 </div> 4 5 <template id="currency"> 6 <span>$<input ref="input" v-bind:value="value" v-on:input="updateValue($event.target.value)" /></span> 7 </template> 8 9 <script type="text/javascript"> 10 Vue.component('currency-input',{ 11 template:'#currency', 12 props: ['value'], 13 methods: { 14 // 不是直接更新值,而是使用此方法來對輸入值進行格式化和位數限制 15 updateValue: function (value) { 16 var formattedValue = value 17 // 刪除兩側的空格符 18 .trim() 19 // 保留 2 小數位 20 .slice(0,value.indexOf('.') === -1 ? value.length: value.indexOf('.') + 3); 21 // 如果值不統一,手動覆蓋以保持一致 22 if (formattedValue !== value) { 23 this.$refs.input.value = formattedValue 24 } 25 // 通過 input 事件發出數值 26 this.$emit('input', Number(formattedValue)) 27 } 28 } 29 }); 30 31 var vm = new Vue({ 32 el:'#demo', 33 data:{ 34 price:'' 35 } 36 }) 37 </script>
(只能輸入小數點后兩位,只能輸入數字)
特殊屬性 ref:
(預期:
string)
ref
被用來給元素或子組件注冊引用信息。引用信息將會注冊在父組件的$refs
對象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素; 如果用在子組件上,引用就指向組件實例:<!-- vm.$refs.p will be the DOM node --> <p ref="p">hello</p> <!-- vm.$refs.child will be the child comp instance --> <child-comp ref="child"></child-comp>當
v-for
用於元素或組件的時候,引用信息將是包含 DOM 節點或組件實例的數組。關於ref注冊時間的重要說明: 因為ref本身是作為渲染結果被創建的,在初始渲染的時候你不能訪問它們 - 它們還不存在!
$refs
也不是響應式的,因此你不應該試圖用它在模版中做數據綁定。
4、定制組件的v-model
(2.2.0新增)
默認情況下,一個組件的 v-model
會使用 value
屬性和 input
事件,但是諸如單選框、復選框之類的輸入類型可能把 value
屬性用作了別的目的。model
選項可以回避這樣的沖突:
Vue.component('my-checkbox', { model: { prop: 'checked', event: 'change' }, props: { checked: Boolean, // this allows using the `value` prop for a different purpose value: String }, // ... })
<my-checkbox v-model="foo" value="some value"></my-checkbox>
上述代碼等價於:
<my-checkbox :checked="foo" @change="val => { foo = val }" value="some value"></my-checkbox>
5、.sync修飾符
在一些情況下,我們可能會需要對一個 prop 進行『雙向綁定』。事實上,這正是 Vue 1.x 中的 .sync
修飾符所提供的功能。當一個子組件改變了一個 prop 的值時,這個變化也會同步到父組件中所綁定的值。這很方便,但也會導致問題,因為它破壞了『單向數據流』的假設。由於子組件改變 prop 的代碼和普通的狀態改動代碼毫無區別,當光看子組件的代碼時,你完全不知道它何時悄悄地改變了父組件的狀態。這在 debug 復雜結構的應用時會帶來很高的維護成本。
上面所說的正是我們在 2.0 中移除 .sync
的理由。但是在 2.0 發布之后的實際應用中,我們發現 .sync
還是有其適用之處,比如在開發可復用的組件庫時。我們需要做的只是讓子組件改變父組件狀態的代碼更容易被區分。
從 2.3.0 起我們重新引入了 .sync
修飾符,但是這次它只是作為一個編譯時的語法糖存在。它會被擴展為一個自動更新父組件屬性的 v-on
偵聽器。
如下代碼
<comp :foo.sync="bar"></comp>
會被擴展為:
<comp :foo="bar" @update:foo="val => bar = val"></comp>
當子組件需要更新 foo
的值時,它需要顯式地觸發一個更新事件:
this.$emit('update:foo', newValue)
6、非父子組件通信
有時候兩個組件也需要通信 (非父子關系)。在簡單的場景下,可以使用一個空的 Vue 實例作為中央事件總線,相當於中轉站,可以用它來傳遞事件和接收事件。
var bus = new Vue() // 觸發組件 A 中的事件 bus.$emit('id-selected', 1) // 在組件 B 創建的鈎子中監聽事件 bus.$on('id-selected', function (id) { // ... })
下面用一個實例來說明:
1 <div id="demo"> 2 <a-component></a-component> 3 <b-component></b-component> 4 </div> 5 6 7 <script type="text/javascript"> 8 Vue.component('a-component',{ 9 template:'<button @click="submit">提交</button>', 10 methods: { 11 submit() { 12 // 事件名字自定義,用不同的名字區別事件 13 this.$root.Bus.$emit('eventName', 123) 14 } 15 } 16 }); 17 18 Vue.component('b-component',{ 19 template:'<p>{{message}}</p>', 20 data:function () { 21 return { 22 message:'b子組件' 23 } 24 }, 25 26 // 當前實例創建完成就監聽這個事件 27 created(){ 28 this.$root.Bus.$on('eventName', function (value) { 29 console.log(value); 30 }); 31 }, 32 33 // 在組件銷毀時別忘了解除事件綁定 34 beforeDestroy() { 35 this.$root.Bus.$off('eventName') 36 } 37 }); 38 39 var vm = new Vue({ 40 el:'#demo', 41 data: { 42 // 空的實例放到根組件下,所有的子組件都能調用 43 Bus: new Vue() 44 } 45 }) 46 </script>
本文的Demo和源代碼已放到GitHub https://github.com/zwl-jasmine95/Vue_test
點個贊唄~~┑( ̄▽  ̄)┍