初學vue2.0-組件
組件
-
組件可以擴展 HTML 元素,封裝可重用的代碼
-
在較高層面上,組件是自定義元素, Vue.js 的編譯器為它添加特殊功能
-
在有些情況下,組件也可以是原生 HTML 元素的形式,以 is 特性擴展。
使用組件
注冊一個全局組件
<div id="example">
<!--web組件的定義脫離了一般的dom元素的寫法,相當于自定義了元素-->
<my-component></my-component>
</div>
// 注冊全局組件,指定之前設定的元素名,然后傳入對象
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// 創建根實例
new Vue({
el: '#example'
})
局部注冊組件
不必在全局注冊每個組件。通過使用組件實例選項注冊,可以使組件僅在另一個實例/組件的作用域中可用
//將傳入給組件的對象單獨寫
var Child = {
template: '<div>A custom component!</div>'
}
new Vue({
//通過components語法創建局部組件
//將組件僅僅放在這個vue實例里面使用
components: {
// <my-component> 將只在父模板可用
'my-component': Child
}
})
DOM模板解析說明
當使用 DOM 作為模版時(例如,將 el 選項掛載到一個已存在的元素上), 你會受到 HTML 的一些限制,
因為 Vue 只有在瀏覽器解析和標準化 HTML 后才能獲取模版內容。
尤其像這些元素 <ul> , <ol>, <table> , <select> 限制了能被它包裹的元素, <option> 只能出現在其它元素內部。
<!--這種是不行的,會報錯-->
<table>
<my-row>...</my-row>
</table>
<!--要通過is屬性來處理-->
<table>
<tr is="my-row"></tr>
</table>
data必須是函數
使用組件時,大多數可以傳入到 Vue 構造器中的選項可以在注冊組件時使用,有一個例外: data 必須是函數。 實際上
//這樣會報錯,提示data必須是一個函數
Vue.component('my-component', {
template: '<span>{{ message }}</span>',
data: {
message: 'hello'
}
})
<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對象引用
data: function () {
return { //字面量寫法會創建新對象
counter: 0
}
}
構成組件
組件意味著協同工作,通常父子組件會是這樣的關系:
-
組件 A 在它的模版中使用了組件 B 。它們之間必然需要相互通信
-
父組件要給子組件傳遞數據,子組件需要將它內部發生的事情告知給父組件
然而,在一個良好定義的接口中盡可能將父子組件解耦是很重要的。這保證了每個組件可以在相對隔離的環境中書寫和理解,也大幅提高了組件的可維護性和可重用性。
在 Vue.js 中,父子組件的關系可以總結為 props down, events up 。
父組件通過 props 向下傳遞數據給子組件,子組件通過 events 給父組件發送消息。看看它們是怎么工作的。
prop
使用prop傳遞數據
-
組件實例的作用域是孤立的。這意味著不能并且不應該在子組件的模板內直接引用父組件的數據。
-
使用 props 把數據傳給子組件。
-
prop 是父組件用來傳遞數據的一個自定義屬性
-
子組件需要顯式地用 props 選項聲明 “prop”
<div id="example-2">
<!--向這個組件傳入一個字符串-->
<child message="hello!"></child>
</div>
Vue.component('child', {
// 聲明 props,用數組形式的對象
props: ['message'],
// 就像 data 一樣,prop 可以用在模板內
// 同樣也可以在 vm 實例中像 “this.message” 這樣使用
template: '<span>{{ message }}</span>'
});
new Vue({
el: '#example-2'
})
動態prop
用 v-bind 動態綁定 props 的值到父組件的數據中。每當父組件的數據變化時,該變化也會傳導給子組件
<div id="example-2">
<!--使用v-modal實現雙向綁定-->
<input v-model="parentMsg">
<br>
<!--需要注意這里使用短橫線的變量,因為在html下是使用短橫線變量的,但是在vue下使用駝峰變量-->
<!--將父組件的parentMsg和子組件的my-message進行綁定-->
<child v-bind:my-message="parentMsg"></child>
</div>
Vue.component('child', {
// 聲明 props
props: ['my-message'],
template: '<span>{{ myMessage }}</span>' //如果寫my-message會報錯,需要轉換為駝峰寫法
});
new Vue({
el: '#example-2',
data: {
parentMsg: ''
}
})
短橫線和駝峰寫法
HTML 特性不區分大小寫。當使用非字符串模版時,prop的名字形式會從 camelCase 轉為 kebab-case(短橫線隔開)
-
在javascript里面使用駝峰寫法,但是在html里面需要轉成短橫線寫法
-
反之亦然,vue會自動處理來自html的短橫線寫法轉為駝峰寫法
字面量語法和動態語法
<!-- 默認只傳遞了一個字符串"1" -->
<comp some-prop="1"></comp>
<!-- 用v-bind實現傳遞實際的數字 -->
<comp v-bind:some-prop="1"></comp></code></pre>
單向數據流
-
prop 是單向綁定的
-
當父組件的屬性變化時,將傳導給子組件,但是不會反過來。這是為了防止子組件無意修改了父組件的狀態——這會讓應用的數據流難以理解。
-
每次父組件更新時,子組件的所有 prop 都會更新為最新值。這意味著你不應該在子組件內部改變 prop 。如果你這么做了,Vue 會在控制臺給出警告。
通常有兩種改變 prop 的情況:
-
prop 作為初始值傳入,子組件之后只是將它的初始值作為本地數據的初始值使用
定義一個局部 data 屬性,并將 prop 的初始值作為局部數據的初始值。
<div id="example-2">
<!--這里用短橫線寫法-->
<child initial-counter="10"></child>
</div>
Vue.component('child', {
props: ['initialCounter'],//這里用駝峰寫法
data: function () { //轉為一個局部變量,寫一個data對象給組件使用
return {counter: this.initialCounter}
},
template: '<span>{{ counter }}</span>'
});
new Vue({
el: '#example-2'
})
-
prop 作為需要被轉變的原始值傳入。
定義一個 computed 屬性,此屬性從 prop 的值計算得出。
//例子沒有寫完,但是根據第一個例子可以知道利用computed的手法原理其實跟寫一個data差不多
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
注意在 JavaScript 中對象和數組是引用類型,指向同一個內存空間,如果 prop 是一個對象或數組,在子組件內部改變它會影響父組件的狀態。
prop驗證
組件可以為 props 指定驗證要求,當組件給其他人使用時這很有用。
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
}
}
}
})
自定義事件
每個 Vue 實例都實現了事件接口(Events interface)
-
使用 $on(eventName) 監聽事件
-
使用 $emit(eventName) 觸發事件
-
父組件可以在使用子組件的地方直接用 v-on 來監聽子組件觸發的事件。
<div id="counter-event-example">
<p>{{ total }}</p>
<!--監聽子組件的事件觸發,監聽increment1事件,處理程序為incrementTotal事件-->
<button-counter v-on:increment1="incrementTotal"></button-counter>
<!--關鍵在于這里v-on綁定的是一個子組件的事件,并且賦值了一個父組件的方法給他,那么子組件里面就可以使用這個方法-->
<button-counter v-on:increment1="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
//監聽click事件,處理程序為increment(子組件定義的方法)
template: '<button v-on:click="increment">{{ counter }}</button>',
//每一個counter都是獨立的對象屬性
data: function () {
return {
counter: 0
}
},
//子組件的方法
methods: {
increment: function () {
this.counter += 1;
//在子組件里面直接觸發之前監聽的increment1事件來執行父組件的方法
this.$emit('increment1');
}
},
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
//父組件的方法
methods: {
incrementTotal: function () {
this.total += 1
}
}
})
1.組件之間因為作用域不同的關系,所以相互獨立,所以子組件想要使用父組件的方法的話需要做一個新的監聽映射
給組件綁定原生事件
<!--代替.on,這么就能夠綁定原生js的事件了-->
<my-component v-on:click.native="doTheThing"></my-component>
使用自定義事件的表單輸入組件
自定義事件也可以用來創建自定義的表單輸入組件,使用 v-model 來進行數據雙向綁定。
所以要讓組件的 v-model 生效,它必須:
-
接受一個 value 屬性
-
在有新的 value 時觸發 input 事件
<!--直接使用v-model,v-modal默認處理input事件-->
<input v-model="something">
<!--v-modal是語法糖,翻譯過來原理是這樣:-->
<!--綁定一個value,然后監聽input事件,通過獲取input的輸入來不斷改變綁定的value的值,滿足了v-modal的觸發條件就可以實現v-modal了-->
<input v-bind:value="something" v-on:input="something = $event.target.value"></code></pre>
一個非常簡單的貨幣輸入:
<!--綁定一個v-model為price,其實是綁定了一個value-->
<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\ //注冊為input,是DOM的節點元素
v-bind:value="value"\ //v-model的value(也是prop)
v-on:input="updateValue($event.target.value)"\ //封裝更新value的函數
>\
</span>\
',
props: ['value'], //父組件將綁定的value傳給子組件
methods: {
// 不是直接更新值,而是使用此方法來對輸入值進行格式化和位數限制
updateValue: function (value) {
var formattedValue = value //對值進行處理
// 刪除兩側的空格符
.trim()
// 保留 2 小數位和2位數
.slice(0, value.indexOf('.') + 3)
// 如果值不統一,手動覆蓋以保持一致,為了保持輸入框顯示內容跟格式化內容一致
if (formattedValue !== value) {
//因為注冊是一個input元素,所以this.$refs 就是input元素
this.$refs.input.value = formattedValue
}
//手動觸發input事件,將格式化后的值傳過去,這是最終顯示輸入框的輸出
this.$emit('input', Number(formattedValue))
}
}
})
//實例化vue實例的
new Vue({
el: '#aa', //要綁定一個vue實例,例如包裹一個id為aa的div
data:{
price:'' //v-model要有數據源
}
})
ref 被用來給元素或子組件注冊引用信息。引用信息會根據父組件的 $refs 對象進行注冊。如果在普通的DOM元素上使用,引用信息就是元素; 如果用在子組件上,引用信息就是組件實例 ref
這是一個比較完整的例子:
<div id="app">
<!--有3個組件,分別不同的v-model-->
<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></code></pre>
Vue.component('currency-input', {
template: '\
<div>\
<label v-if="label">{{ label }}</label>\
$\
<input\
ref="input"\ // 這些沒什么特別,引用注冊為input DOM元素
v-bind:value="value"\
v-on:input="updateValue($event.target.value)"\
v-on:focus="selectAll"\ //這里多了focus事件監聽,焦點在的時候全選,也只是多了處理而已,對整體邏輯理解沒啥影響
v-on:blur="formatValue"\ //這里多了blur事件監聽,焦點離開的時候格式化
>\
</div>\
',
props: { //多個prop傳遞,因為prop是對象,只要是對象格式就行
value: {
type: Number,
default: 0
},
label: {
type: String,
default: ''
}
},
mounted: function () { //這是vue的過渡狀態,暫時忽略不影響理解
this.formatValue()
},
methods: {
updateValue: function (value) {
var result = currencyValidator.parse(value, this.value)
if (result.warning) {
// 這里也使用了$refs獲取引用注冊信息
this.$refs.input.value = result.value
}
this.$emit('input', result.value)
},
formatValue: function () {
this.$refs.input.value = currencyValidator.format(this.value) //這里注意下,這個this是prop傳遞過來的,也相當于這個組件作用域
},
selectAll: function (event) { //event可以獲取原生的js事件
// 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)
}
}
})</code></pre>
非父子組件通信
在簡單的場景下,使用一個空的 Vue 實例作為中央事件總線:
var bus = new Vue()
// 觸發組件 A 中的事件
bus.$emit('id-selected', 1)
/
通過on來監聽子組件的事件來實現傳遞/
// 在組件 B 創建的鉤子中監聽事件
bus.$on('id-selected', function (id) {
// ...
})</code></pre>
使用Slot分發內容
為了讓組件可以組合,我們需要一種方式來混合父組件的內容與子組件自己的模板。這個過程被稱為 內容分發 (或 “transclusion” 如果你熟悉 Angular)
編譯作用域
組件作用域簡單地說是:父組件模板的內容在父組件作用域內編譯;子組件模板的內容在子組件作用域內編譯。
假定 someChildProperty 是子組件的屬性,上例不會如預期那樣工作。父組件模板不應該知道子組件的狀態。
<!-- 無效 -->
<child-component v-show="someChildProperty"></child-component>
如果要綁定子組件內的指令到一個組件的根節點,應當在它的模板內這么做:
Vue.component('child-component', {
// 有效,因為是在正確的作用域內
template: '<div v-show="someChildProperty">Child</div>',
data: function () {
return { //因為這個屬性在當前組件內編譯(創建了)
someChildProperty: true
}
}
})
類似地,分發內容是在父組件作用域內編譯。
單個Slot
-
除非子組件模板包含至少一個 <slot> 插口,否則父組件的內容將會被丟棄。
-
當子組件模板只有一個沒有屬性的 slot 時,父組件整個內容片段將插入到 slot 所在的 DOM 位置,并替換掉 slot 標簽本身。
-
備用內容在子組件的作用域內編譯,并且只有在宿主元素為空,且沒有要插入的內容時才顯示備用內容。
<!--父組件模版:-->
<div id="aa">
<h1>我是父組件的標題</h1>
<!--子組件的作用域內編譯,宿主元素為空,且沒有要插入的內容-->
<my-component></my-component>
<my-component>
<p>這是一些初始內容</p>
<p>這是更多的初始內容</p>
</my-component>
</div>
Vue.component('my-component', {
//my-component 組件有下面模板
template: '\
<div>\
<h2>我是子組件的標題</h2> \
<slot> \ //有slot插口,所以沒有被父組件丟棄
只有在沒有要分發的內容時才會顯示。\
</slot> \
</div> \
'
})
new Vue({
el: '#aa',
})
渲染結果:
<div id="aa"><h1>我是父組件的標題</h1>
<div>
<h2>我是子組件的標題</h2>
<!--這里是直接插入,沒有使用DOM元素-->
只有在沒有要分發的內容時才會顯示。
</div>
<div>
<h2>我是子組件的標題</h2>
<p>這是一些初始內容</p>
<p>這是更多的初始內容</p>
</div>
</div>
有名字的Slot
-
<slot> 元素可以用一個特殊的屬性 name 來配置如何分發內容。多個 slot 可以有不同的名字。具名 slot 將匹配內容片段中有對應 slot 特性的元素。
-
仍然可以有一個匿名 slot ,它是默認 slot ,作為找不到匹配的內容片段的備用插槽。如果沒有默認的 slot ,這些找不到匹配的內容片段將被拋棄。
<div id="aa">
<app-layout>
<!--這是header-->
<h1 slot="header">這里可能是一個頁面標題</h1>
<p>主要內容的一個段落。</p>
<p>另一個主要段落。</p>
<!--這是footer-->
<p slot="footer">這里有一些聯系信息</p>
</app-layout>
</div>
Vue.component('app-layout', {
template: '\
<div class="container"> \
<header> \ //找到名字叫header的slot之后替換內容,這里替換的是整個DOM
<slot name="header"></slot> \
</header> \
<main> \ //因為slot沒有屬性,會將內容插入到slot的所在的DOM位置
<slot></slot> \
</main> \
<footer>\ //跟header類似
<slot name="footer"></slot> \
</footer> \
</div> \
'
});
new Vue({
el: '#aa',
})
渲染結果為:
<div class="container">
<header>
<h1>這里可能是一個頁面標題</h1>
</header>
<main>
<p>主要內容的一個段落。</p>
<p>另一個主要段落。</p>
</main>
<footer>
<p>這里有一些聯系信息</p>
</footer>
</div>
作用域插槽(vue2.1)
-
作用域插槽是一種特殊類型的插槽,用作使用一個(能夠傳遞數據到)可重用模板替換已渲染元素。
-
在子組件中,只需將數據傳遞到插槽,就像你將 prop 傳遞給組件一樣
-
在父級中,具有特殊屬性 scope 的 <template> 元素,表示它是作用域插槽的模板。scope 的值對應一個臨時變量名,此變量接收從子組件中傳遞的 prop 對象
<div id="parent" class="parent">
<child>
<!--接收從子組件中傳遞的prop對象(這個就是作用域插槽)-->
<template scope="props">
<span>hello from parent</span>
<!--使用這個prop對象-->
<span>{{ props.text }}</span>
</template>
</child>
</div>
Vue.component('child', {
props: ['props'], //這個寫不寫都可以,作用域插槽固定會接收prop對象,而且這個prop對象是肯定存在的
template: '\
<div class="child"> \
<slot text="hello from child"></slot> \ //在子組件里直接將數據傳遞給slot
</div> \
'
});
new Vue({
el: '#parent',
})
渲染結果:
<div class="parent">
<div class="child">
<span>hello from parent</span>
<!--子組件的東西出現在這里了-->
<span>hello from child</span>
</div>
</div>
另外一個例子,作用域插槽更具代表性的用例是列表組件,允許組件自定義應該如何渲染列表每一項
<div id="parent">
<!--綁定一個組件的prop ,位置1-->
<my-awesome-list :items="items">
<!-- 作用域插槽也可以在這里命名 -->
<!--這里props只代表確定接受prop對象的東西,不關注prop對象里面有什么,位置2-->
<template slot="item" scope="props">
<li class="my-fancy-item">{{ props.text }}</li>
</template>
</my-awesome-list>
</div>
Vue.component('my-awesome-list', {
props:['items'], //需要聲明prop為items,需要是為下面的循環遍歷的items的數據源做設定,位置3
template: '\
<ul> \
<slot name="item" v-for="item in items" :text="item.text"> \ //在slot中,循環遍歷輸出items的text,位置4
</slot> \
</ul> \
'
});
new Vue({
el: '#parent',
data : {
items:[ //初始化items數據
{text:"aa"},
{text:"bb"}
]
}
})
-
位置1,實現了一個組件的prop綁定,prop需要在組件里面聲明,這里綁定的是items,這是要將父組件的items傳遞到子組件,所以在位置3里面需要聲明,在vue實例要初始化
-
位置2,這里scope的props是代表作用域插槽接收來自prop對象的數據,props.text是代表每一個li要輸出的是prop對象的text屬性
-
位置3,在組件里聲明props,為了接收父組件綁定的items屬性,然后將其給位置4的循環使用
-
位置4,這里綁定了text屬性,就是前呼位置2里面輸出的prop對象的text屬性
動態組件
多個組件可以使用同一個掛載點,然后動態地在它們之間切換。使用保留的 <component> 元素,動態地綁定到它的 is 特性
var vm = new Vue({
el: '#example',
data: {
currentView: 'home' //默認值
},
components: { //根據不同的值進行不同的組件切換,這里用components寫法
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
<!--這個is是一個字符串,根據返回值來給組件進行v-bind-->
<component v-bind:is="currentView">
<!-- 組件在 vm.currentview 變化時改變! -->
</component>
keep-alive
如果把切換出去的組件保留在內存中,可以保留它的狀態或避免重新渲染。為此可以添加一個 keep-alive 指令參數
<keep-alive>
<component :is="currentView">
<!-- 非活動組件將被緩存! -->
</component>
</keep-alive>
雜項
編寫可復用組件
在編寫組件時,記住是否要復用組件有好處。一次性組件跟其它組件緊密耦合沒關系,但是可復用組件應當定義一個清晰的公開接口。
Vue 組件的 API 來自三部分 - props, events 和 slots :
-
Props 允許外部環境傳遞數據給組件
-
Events 允許組件觸發外部環境的副作用
-
Slots 允許外部環境將額外的內容組合在組件中。
<!--v-bind,縮寫:,綁定prop-->
<!--v-on,縮寫@,監聽事件-->
<!--slot插槽-->
<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat"
>
<img slot="icon" src="...">
<p slot="main-text">Hello!</p>
</my-component>
子組件索引
盡管有 props 和 events ,但是有時仍然需要在 JavaScript 中直接訪問子組件。為此可以使用 ref 為子組件指定一個索引 ID 。
<div id="parent">
<user-profile ref="profile"></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 訪問子組件
var child = parent.$refs.profile
-
當 ref 和 v-for 一起使用時, ref 是一個數組或對象,包含相應的子組件。
-
$refs 只在組件渲染完成后才填充,并且它是非響應式的。它僅僅作為一個直接訪問子組件的應急方案——應當避免在模版或計算屬性中使用 $refs 。
-
ref 被用來給元素或子組件注冊引用信息。引用信息會根據父組件的 $refs 對象進行注冊。如果在普通的DOM元素上使用,引用信息就是元素; 如果用在子組件上,引用信息就是組件實例 ref
組件命名約定
-
當注冊組件(或者 props)時,可以使用 kebab-case ,camelCase ,或 TitleCase 。Vue 不關心這個。
-
在 HTML 模版中,請使用 kebab-case 形式:
// 在組件定義中
components: {
// 使用 kebab-case 形式注冊--橫線寫法
'kebab-cased-component': { /* ... */ },
// register using camelCase --駝峰寫法
'camelCasedComponent': { /* ... */ },
// register using TitleCase --標題寫法
'TitleCasedComponent': { /* ... */ }
}
<!-- 在HTML模版中始終使用 kebab-case--橫線寫法 -->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<title-cased-component></title-cased-component>
遞歸組件
-
組件在它的模板內可以遞歸地調用自己,不過,只有當它有 name 選項時才可以
-
當你利用Vue.component全局注冊了一個組件, 全局的ID作為組件的 name 選項,被自動設置.
//組件可以用name來寫名字
name: 'unique-name-of-my-component'
//也可以在創建的時候默認添加名字
Vue.component('unique-name-of-my-component', {
// ...
})
//如果同時使用的話,遞歸的時候就會不斷遞歸自己,導致溢出
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
使用-v-once-的低級靜態組件-Cheap-Static-Component
盡管在 Vue 中渲染 HTML 很快,不過當組件中包含大量靜態內容時,可以考慮使用 v-once 將渲染結果緩存起來,就像這樣:
Vue.component('terms-of-service', {
template: '\
<div v-once>\
<h1>Terms of Service</h1>\
... a lot of static content ...\
</div>\
'
})
v-once只渲染元素和組件一次。隨后的重新渲染,元素/組件及其所有的子節點將被視為靜態內容并跳過。這可以用于優化更新性能。
來自:https://segmentfault.com/a/1190000008251348