一.什麼是組件? 組件 (Component) 是 Vue.js 最強大的功能之一。組件可以擴展 HTML 元素,封裝可重用的代碼。在較高層面上,組件是自定義元素,Vue.js 的編譯器為它添加特殊功能。在有些情況下,組件也可以表現為用 is 特性進行了擴展的原生 HTML 元素。 所有的 Vue ...
一.什麼是組件?
組件 (Component) 是 Vue.js 最強大的功能之一。組件可以擴展 HTML 元素,封裝可重用的代碼。在較高層面上,組件是自定義元素,Vue.js 的編譯器為它添加特殊功能。在有些情況下,組件也可以表現為用 is
特性進行了擴展的原生 HTML 元素。
所有的
Vue 組件
同時也都是Vue 的實例
,所以可接受相同的選項對象 (除了一些根級特有的選項) 並提供相同的生命周期鉤子。
二.註冊組件
全局註冊
html代碼:
<div id="example"> <my-component></my-component> </div>
JS代碼:
// 註冊 Vue.component('my-component', { template: '<div>A custom component!</div>' }) var vm = new Vue({ el: '#example', data: { } })
渲染結果為:
<div id="example"> <div>A custom component!</div> </div>
或者另外一種註冊方式,通過 全局API:Vue.extend()
代碼如下:
// 註冊 var MyComponent = Vue.extend({ template: '<div>A custom component!</div>' }); // 註冊 Vue.component('my-component', MyComponent); var vm = new Vue({ el: '#example', data: { } })
Vue.extend()使用說明
下麵說明下Vue.extend( options )
的使用。
參數:{Object} options
用法:使用基礎 Vue 構造器,創建一個“子類”。參數是一個包含組件選項的對象
。data
選項是特例,需要註意 - 在 Vue.extend()
中它必須是函數
。
<div id="mount-point"></div>
// 創建構造器 var Profile = Vue.extend({ template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>', data: function () { return { firstName: 'Walter', lastName: 'White', alias: 'Heisenberg' } } }) // 創建 Profile 實例,並掛載到一個元素上。 new Profile().$mount('#mount-point')
結果如下:
<p>Walter White aka Heisenberg</p>
上面又用到了實例方法vm.$mount()
,下麵說明下它的使用方式。
vm.$mount( [elementOrSelector] )使用說明
參數:
{Element | string} [elementOrSelector]
{boolean} [hydrating]
返回值:vm
- 實例自身
用法:
如果 Vue
實例在實例化時沒有收到 el
選項,則它處於“未掛載
”狀態,沒有關聯的 DOM
元素。可以使用 vm.$mount()
手動地掛載一個未掛載的實例。
如果沒有提供 elementOrSelector
參數,模板將被渲染為文檔之外的的元素,並且你必須使用原生 DOM API
把它插入文檔中。
這個方法返回實例自身,因而可以鏈式調用其它實例方法。
var MyComponent = Vue.extend({ template: '<div>Hello!</div>' }) // 創建並掛載到 #app (會替換 #app) new MyComponent().$mount('#app') // 同上 new MyComponent({ el: '#app' }) // 或者,在文檔之外渲染並且隨後掛載 var component = new MyComponent().$mount() document.getElementById('app').appendChild(component.$el)
局部註冊
你不必把每個組件都註冊到全局。你可以通過某個 Vue 實例/組件的實例選項 components 註冊僅在其作用域中可用的組件:
var Child = { template: '<div>A custom component!</div>' } new Vue({ // ... components: { // <my-component> 將只在父組件模板中可用 'my-component': Child } })
這種封裝也適用於其它可註冊的 Vue 功能,比如指令
。
DOM 模板解析註意事項
像 <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
組件
因此,請儘可能使用字元串模板。
data 必須是函數
如下代碼:
Vue.component('my-component', { template: '<span>{{ message }}</span>', data: { message: 'hello' } })
那麼 Vue 會停止運行,併在控制台發出警告,告訴你在組件實例中 data
必須是一個函數。
我們來理解下,看下麵代碼:
<div id="example-2"> <simple-counter></simple-counter> <simple-counter></simple-counter> <simple-counter></simple-counter> </div>
var data = { counter: 0 } Vue.component('simple-counter', { template: '<button v-on:click="counter += 1">{{ counter }}</button>', // 技術上 data 的確是一個函數了,因此 Vue 不會警告, // 但是我們卻給每個組件實例返回了同一個對象的引用 data: function () { return data } }) new Vue({ el: '#example-2' })
由於這三個組件實例共用
了同一個 data
對象,因此遞增一個 counter
會影響所有組件!這就錯了。我們可以通過為每個組件返回全新的數據對象來修複這個問題:
data: function () { return { counter: 0 } }
現在每個 counter 都有它自己內部的狀態了,不會相互影響。
三.Prop
最常見的應用就是:組件 A 在它的模板中使用了組件 B。它們之間必然需要相互通信:父組件可能要給子組件下發數據,子組件則可能要將它內部發生的事情告知父組件。
在 Vue 中,父子組件的關係可以總結為 prop
向下傳遞,事件
向上傳遞。父組件通過 prop
給子組件下發數據,子組件通過事件
給父組件發送消息。
使用 Prop 傳遞數據
實例1:
Vue.component('child', { // 聲明 props props: ['message'], // 就像 data 一樣,prop 也可以在模板中使用 // 同樣也可以在 vm 實例中通過 this.message 來使用 template: '<span>{{ message }}</span>' }) <child message="hello!"></child>
結果:hello!
實例2:
HTML 特性是不區分大小寫的。所以,當使用的不是字元串模板時,camelCase (駝峰式命名) 的 prop 需要轉換為相對應的 kebab-case (短橫線分隔式命名):
Vue.component('child', { // 在 JavaScript 中使用 camelCase props: ['myMessage'], template: '<span>{{ myMessage }}</span>' }) <!-- 在 HTML 中使用 kebab-case --> <child my-message="hello!"></child>
如果你使用字元串模板,則沒有這些限制。
動態 Prop
<div> <input v-model="parentMsg"> <br> <child v-bind:my-message="parentMsg"></child> </div>
你也可以使用 v-bind 的縮寫語法:
<child :my-message="parentMsg"></child>
(重要)
如果你想把一個對象
的所有屬性
作為 prop 進行傳遞,可以使用不帶任何參數的 v-bind
(即用 v-bind 而不是 v-bind:prop-name)。例如,已知一個 todo
對象:
todo: { text: 'Learn Vue', isComplete: false }
然後:
<todo-item v-bind="todo"></todo-item>
將等價於:
<todo-item v-bind:text="todo.text" v-bind:is-complete="todo.isComplete" ></todo-item>
字面量語法 vs 動態語法
初學者常犯的一個錯誤是使用字面量語法傳遞數值:
<!-- 傳遞了一個字元串 "1" --> <comp some-prop="1"></comp>
因為它是一個字面量 prop
,它的值是字元串 "1" 而不是一個數值。如果想傳遞一個真正的 JavaScript 數值,則需要使用 v-bind
,從而讓它的值被當作 JavaScript 表達式
計算:
<!-- 傳遞真正的數值 --> <comp v-bind:some-prop="1"></comp>
單向數據流
Prop 是單向綁定的:當父組件的屬性變化時,將傳導給子組件,但是反過來不會。這是為了防止子組件無意間修改了父組件的狀態,來避免應用的數據流變得難以理解。
另外,每次父組件更新時,子組件的所有 prop 都會更新為最新值。這意味著你不應該
在子組件內部改變 prop
。如果你這麼做了,Vue 會在控制台給出警告。
在兩種情況下,我們很容易忍不住想去修改 prop 中數據:
- Prop 作為初始值傳入後,子組件想把它當作局部數據來用;
- Prop 作為原始數據傳入,由子組件處理成其它數據輸出。
對這兩種情況,正確的應對方式是:
1.定義一個局部變數,並用 prop 的值初始化它:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
2.定義一個計算屬性,處理 prop 的值並返回:
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
註意在 JavaScript 中
對象
和數組
是引用類型,指向同一個記憶體空間
,如果 prop 是一個對象或數組,在子組件內部改變它會影響
父組件的狀態。
Prop 驗證
我們可以為組件的 prop 指定驗證規則。如果傳入的數據不符合要求,Vue 會發出警告。
要指定驗證規則,需要用對象的形式
來定義 prop,而不能用字元串數組
:
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 會拋出警告 (如果使用的是開發版本)。註意 prop 會在組件實例創建之前進行校驗,所以在 default
或 validator
函數里,諸如 data
、computed
或 methods
等實例屬性還無法使用。
其它實例:
Vue.component('modal', { template: '#modal-template', props: { show: { type: Boolean, required: true, twoWay: true } } });
twoWay Prop
的參數 移除
。Props 現在只能單向傳遞。為了對父組件產生反向影響,子組件需要顯式地傳遞一個事件而不是依賴於隱式地雙向綁定。所以上面的的最後一個實例只是貼出來代碼而已,最新版本已經移除了。
四.非Prop特性
所謂非 prop 特性,就是指它可以直接傳入組件,而不需要定義相應的 prop。
儘管為組件定義明確的 prop 是推薦的傳參方式,組件的作者卻並不總能預見到組件被使用的場景。所以,組件可以接收任意傳入的特性
,這些特性都會被添加到組件的根元素上
。
例如,假設我們使用了第三方組件 bs-date-input,它包含一個 Bootstrap 插件,該插件需要在 input 上添加 data-3d-date-picker 這個特性。這時可以把特性直接添加到組件上 (不需要事先定義 prop):
<bs-date-input data-3d-date-picker="true"></bs-date-input>
添加屬性 data-3d-date-picker="true"
之後,它會被自動添加到 bs-date-input
的根元素上。
假設這是 bs-date-input 的模板:
<input type="date" class="form-control">
為了給該日期選擇器插件增加一個特殊的主題,我們可能需要增加一個特殊的 class,比如:
<bs-date-input data-3d-date-picker="true" class="date-picker-theme-dark" ></bs-date-input>
最終在根元素上生成的class值為:form-control date-picker-theme-dark。
五.自定義事件
我們知道,父組件使用 prop 傳遞數據給子組件。但子組件怎麼跟父組件通信呢?這個時候 Vue 的自定義事件系統就派得上用場了。
使用 v-on 綁定自定義事件
每個 Vue 實例都實現了事件介面,即:
- 使用 $on(eventName) 監聽事件
- 使用 $emit(eventName) 觸發事件
Vue 的事件系統與瀏覽器的
EventTarget API
有所不同。儘管它們的運行起來類似,但是$on
和$emit
並不是addEventListener
和dispatchEvent
的別名。
另外,父組件可以在使用子組件的地方直接用 v-on
來監聽子組件觸發的事件。
不能用 $on 偵聽子組件釋放的事件,而必須在模板里直接用 v-on 綁定,參見下麵的例子。
<div id="counter-event-example"> <p>{{ total }}</p> <button-counter v-on:increment="incrementTotal"></button-counter> <button-counter v-on:increment="incrementTotal"></button-counter> </div>
Vue.component('button-counter', { template: '<button v-on:click="incrementCounter">{{ counter }}</button>', data: function () { return { counter: 0 } }, methods: { incrementCounter: function () { this.counter += 1 this.$emit('increment') } }, }) new Vue({ el: '#counter-event-example', data: { total: 0 }, methods: { incrementTotal: function () { this.total += 1 } } })
給組件綁定原生事件.native
有時候,你可能想在某個組件的根元素上監聽一個原生事件。可以使用 v-on 的修飾符 .native
。例如:
<my-component v-on:click.native="doTheThing"></my-component>
.sync 修飾符(2.3.0+)
<comp :foo.sync="bar"></comp>
會被擴展為:
<comp :foo="bar" @update:foo="val => bar = val"></comp>
當子組件需要更新 foo 的值時,它需要顯式地觸發一個更新事件:
this.$emit('update:foo', newValue)
使用自定義事件的表單輸入組件
自定義事件可以用來創建自定義的表單輸入組件,使用 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>
所以要讓組件的 v-model 生效,它應該 (從 2.2.0 起是可配置的):
- 接受一個
value prop
- 在有新的值時觸發
input
事件並將新值作為參數
例子1:
<div id="app"> <custom-input v-model="something"></custom-input> <br/> {{something}} </div>
// 註冊 Vue.component('custom-input', { props:['something'], template: '<input type="text" v-bind:value="something" v-on:input="updateValue($event.target.value)"/>', methods:{ updateValue:function(value){ this.$emit('input', value) } } }) var vm = new Vue({ el: '#app', data: { something:'' } })
例子2:貨幣輸入的自定義控制項
<currency-input v-model="price"></currency-input>
Vue.component('currency-input', { template: '\ <span>\ $\ <input\ ref="input"\ v-bind:value="value"\ v-on:input="updateValue($event.target.value)"\ >\ </span>\ ', props: ['value'], methods: { // 不是直接更新值,而是使用此方法來對輸入值進行格式化和位數限制 updateValue: function (value) { var formattedValue = value // 刪除兩側的空格符 .trim() // 保留 2 位小數 .slice( 0, value.indexOf('.') === -1 ? value.length : value.indexOf('.') + 3 ) // 如果值尚不合規,則手動覆蓋為合規的值 if (formattedValue !== value) { this.$refs.input.value = formattedValue } // 通過 input 事件帶出數值 this.$emit('input', Number(formattedValue)) } } })
實例3:更加完善的貨幣過濾器
<div id="app"> <currency-input label="Price" v-model="price" ></currency-input> <currency-input label="Shipping" v-model="shipping" ></currency-input> <currency-input label="Handling" v-model="handling" ></currency-input> <currency-input label="Discount" v-model="discount" ></currency-input> <p>Total: ${{ total }}</p> </div>
Vue.component('currency-input', { template: '\ <div>\ <label v-if="label">{{ label }}</label>\ $\ <input\ ref="input"\ v-bind:value="value"\ v-on:input="updateValue($event.target.value)"\ v-on:focus="selectAll"\ v-on:blur="formatValue"\ >\ </div>\ ', props: { value: { type: Number, default: 0 }, label: { type: String, default: '' } }, mounted: function () { this.formatValue() }, methods: { updateValue: function (value) { var result = currencyValidator.parse(value, this.value) if (result.warning) { this.$refs.input.value = result.value } this.$emit('input', result.value) }, formatValue: function () { this.$refs.input.value = currencyValidator.format(this.value) }, selectAll: function (event) { // Workaround for Safari bug // http://stackoverflow.com/questions/1269722/selecting-text-on-focus-using-jquery-not-working-in-safari-and-chrome setTimeout(function () { event.target.select() }, 0) } } }) new Vue({ el: '#app', data: { price: 0, shipping: 0, handling: 0, discount: 0 }, computed: { total: function () { return (( this.price * 100 + this.shipping * 100 + this.handling * 100 - this.discount * 100 ) / 100).toFixed(2) } } })
自定義組件的 v-model(2.2.0 新增)
預設情況下,一個組件的 v-model 會使用 value prop 和 input 事件。但是諸如單選框、覆選框之類的輸入類型可能把 value 用作了別的目的。model 選項可以避免這樣的衝突:
Vue.component('my-checkbox', { model: { prop: 'checked', event: 'change' }, props: { checked: Boolean, // 這樣就允許拿 `value` 這個 prop 做其它事了 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>
註意你仍然需要顯式聲明
checked
這個prop
。
完整的代碼:
html:
<div id="app"> <my-checkbox v-model="foo" value="some value"></my-checkbox> {{foo}} </div>
JS代碼:
Vue.component('my-checkbox', { model: { prop: 'checked', event: 'change' }, props: { checked: Boolean, // 這樣就允許拿 `value` 這個 prop 做其它事了 value: String }, template:'<input type="checkbox" @change="changefun(ischecked)"/>', data:function(){ return { ischecked:this.checked } }, methods:{ changefun(state){ this.ischecked = !state; this.$emit('change', this.ischecked); } } }) var vm = new Vue({ el: '#app', data: { foo:false } })
非父子組件的通信
有時候,非父子關係的兩個組件之間也需要通信。在簡單的場景下,可以使用一個空的 Vue 實例作為事件匯流排:
var bus = new Vue()
// 觸發組件 A 中的事件 bus.$emit('id-selected', 1)
// 在組件 B 創建的鉤子中監聽事件 bus.$on('id-selected', function (id) { // ... })
在複雜的情況下,我們應該考慮使用專門的狀態管理模式Vuex
。
來看一個完整的例子:
html代碼:
<div id="app"> <comp-a v-on:id-selected="getdate"></comp-a> <comp-b></comp-b> </div>
JS代碼:
var bus = new Vue(); Vue.component('comp-a', { template:'<button class="compa" @click="comfuna">組件A</button>', data:function(){ return { } }, methods:{ comfuna(){ bus.$emit('id-selected', 1); this.$emit('id-selected', 1); } } }) Vue.component('comp-b', { template:'<div class="compb">組件B</div>', data:function(){ return { } }, mounted(){ // 在組件 B 創建的鉤子中監聽事件 bus.$on('id-selected', function (id) { console.log('在B組件中得到的值:'+id); }) } }) var vm = new Vue({ el: '#app', data: {}, methods:{ getdate(value){ console.log('得到當前的值:'+value); } } })
六.使用插槽slot分發內容
在使用組件時,我們常常要像這樣組合它們:
<app> <app-header></app-header> <app-footer></app-footer> </app>
註意兩點:
<app>
組件不知道它會收到什麼內容。這是由使用<app>
的父組件決定的。<app>
組件很可能有它自己的模板。
為了讓組件可以組合,我們需要一種方式來混合父組件的內容與子組件自己的模板。使用特殊的 <slot>
元素作為原始內容的插槽。
一個常見錯誤是試圖在父組件模板內將一個指令綁定到子組件的屬性/方法:
<!-- 無效 --> <child-component v-show="someChildProperty"></child-component>
正確做法:
Vue.component('child-component', { // 有效,因為是在正確的作用域內 template: '<div v-show="someChildProperty">Child</div>', data: function () { return { someChildProperty: true } } })
單個插槽
假定 my-component 組件有如下模板:
<div> <h2>我是子組件的標題</h2> <slot> 只有在沒有要分發的內容時才會顯示。 </slot> </div>
父組件模板:
<div> <h1>我是父組件的標題</h1> <my-component> <p>這是一些初始內容</p> <p>這是更多的初始內容</p> </my-component> </div>
渲染結果:
<div> <h1>我是父組件的標題</h1> <div> <h2>我是子組件的標題</h2> <p>這是一些初始內容</p> <p>這是更多的初始內容</p> </div> </div>
具名插槽
<slot>
元素可以用一個特殊的特性 name
來進一步配置如何分發內容。多個插槽可以有不同的名字。具名插槽將匹配內容片段中有對應 slot
特性的元素。
仍然可以有一個匿名插槽,它是預設插槽,作為找不到匹配的內容片段的備用插槽。如果沒有預設插槽,這些找不到匹配的內容片段將被拋棄。
例如,假定我們有一個 app-layout 組件,它的模板為:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
父組件模板:
<app-layout> <h1 slot="header">這裡可能是一個頁面標題</h1> <p>主要內容的一個段落。</p> <p>另一個主要段落。</p> <p slot="footer">這裡有一些聯繫信息</p> </app-layout>
渲染結果為:
<div class="container"> <header> <h1>這裡可能是一個頁面標題</h1> </header> <main> <