? ? ? 組件(Component)是Vue.js最核心的功能,也是整個(gè)架構(gòu)設(shè)計(jì)最精彩的地方,當(dāng)然也是最難掌握的。本章將帶領(lǐng)你由淺入深地學(xué)習(xí)組件的全部?jī)?nèi)容,并通過(guò)幾個(gè)實(shí)戰(zhàn)項(xiàng)目熟練使用Vue組件。
7.1組件與復(fù)用
7.1.1為什么使用組件? ?
? ? ? ? 組建可以提高一些控件、JavaScript能力的復(fù)用,提高組件重用性,讓代碼可復(fù)用。我們先寫(xiě)一個(gè)聊天頁(yè)面的,示例代碼如下:
<Card style="width:350px">
? ? <p slot="title">與XXX聊天</p>
? ? <a href="#" slot="extra">
? ? ? ? <Icon type="android-close" size="18"></Icon>
? ? </a>
? ? <div style="height:100px"></div>
? ? <div>
? ? ? ? <Row :gutter="16">
? ? ? ? ? ? <i-col span="17">
? ? ? ? ? ? ? ? <i-input v-model="value" placeholder="請(qǐng)輸入……"</i-input>
? ? ? ? ? ? </i-col>
? ? ? ? ? ? <i-col span="4">
? ? ? ? ? ? ? ? <i-button type="primary" icon="paper-airplane"發(fā)送</i-button>
? ? ? ? ? ? </i-col>
? ? ? ? </Row>
? ? </div>
</Card>
? ? ? ? 是不是很奇怪,有很多我們從未使用過(guò)的標(biāo)簽,比如<Card>、<Row>、<i-col>、<i-input>、<i-button>等,而且整段代碼除了內(nèi)聯(lián)的幾個(gè)樣式外,一句CSS代碼也沒(méi)有。
? ? ? ? 這些沒(méi)見(jiàn)過(guò)的自定義標(biāo)簽就是組件,每個(gè)標(biāo)簽代表一個(gè)組件,在任何時(shí)候使用Vue的地方都可以直接使用。接下來(lái),我們就來(lái)看看組件的具體用法。
7.1.2組建用法
? ? ? ? 回顧創(chuàng)建Vue示例的方法,我們發(fā)現(xiàn)組件與之類(lèi)似,需要注冊(cè)后才可以使用。注冊(cè)有全局注冊(cè)和局部注冊(cè)兩種方式,全局注冊(cè)后,任何Vue實(shí)例都可以使用,全局注冊(cè)示例代碼如下:
? ? ? ? Vue.component('my-component',{
? ? ? ? ? ? //my-component就是注冊(cè)的組件自定義標(biāo)簽名稱(chēng),推薦使用小寫(xiě)加減號(hào)分隔符的形式命名
? ? });
? ? ? ? 要在父實(shí)例中使用這個(gè)組件,必須要在實(shí)例創(chuàng)建前注冊(cè),之后就可以用<my-component>來(lái)使用組件,實(shí)例代碼如下:
? ? <div id="app">
? ? ? ? <my-component></my-component >
? ? <div>
? ? <script>
? ? ? Vue.component("my-component",{
? ? ? ? ? template:'<div>這里是組件內(nèi)容</div>'
//template的DOM結(jié)構(gòu)必須被一個(gè)元素包含,如果直接寫(xiě)成“這里是組件的內(nèi)容”,不帶"<div></div>是無(wú)法被渲染的“
? ? });
? ? var app = new Vue({
? ? ? ? el:'#app'
? ? })
<script>
? ? ? ? 在Vue實(shí)例中,使用components選項(xiàng)可以局部注冊(cè)組件,注冊(cè)后的組件只有在該實(shí)例作用域下有效。組件中也可也使用components選項(xiàng)來(lái)注冊(cè)組件,使組件可以嵌套。示例代碼如下:
? ? <div id="app">
? ? ? ? <my-component></my-component>
? ? </div>
? ? <script>
? ? ? var Child={
? ? ? ? template:'<div>局部注冊(cè)組件的內(nèi)容</div>
? ? }
? ? ? ? var app = new Vue({
? ? ? ? el:'#app',
? ? ? ? compoments:{
? ? ? ? ? ? 'my-component':Child
? ? ? ? }
? ? });
? ? ? ? Vue組件的模板在某些情況下會(huì)受到HTML的影響,比如<table>內(nèi)規(guī)定只允許是<tr>、<td>、<th>等這些表格元素,所以在<table>內(nèi)直接使用組件是無(wú)效的。這種情況下,可以使用特殊的屬性來(lái)掛載組件,示例代碼如下:
? ? <div id="app>>
? ? ? ? <table>
? ? ? ? ? ? <tbody is="my-component"></tbody>
? ? ? ? </table>
? ? </div>
? ? <script>
? ? ? ? Vue.componnt("my-compone",{
? ? ? ? ? ? template:'<div>這里是組件里的內(nèi)容</div>"
? ? });
? ? ? ? var app =new Vue({
? ? ? ? ? ? el:'#app'
? ? })
? ? </script>
? ? tips:如果使用的是字符串模板,是不受限制的,比如后面章節(jié)介紹的.VUE單文件用法等。
? ? 除了template選項(xiàng)外,組件中還可以像Vue實(shí)例那樣使用其它的選項(xiàng),比如data、computed、methods。但是在使用data時(shí),和實(shí)例稍有區(qū)別,data必須是函數(shù),然后將數(shù)據(jù)return出去。例如:
<div id="app">
? ? <my-component></my-component>
</div>
<script>
? ? Vue.component('my-component',{
? ? ? ? template:'<div>message</div>',
? ? ? ? data:function(){
? ? ? ? ? ? return {
? ? ? ? ? ? ? ? message:'組件內(nèi)容'
? ? ? ? }
? ? }
});
……
JavaScript對(duì)象是引用關(guān)系,所以如果return出的對(duì)象引用了外部的一個(gè)對(duì)象,那這個(gè)對(duì)象就是共享的,任何一部分修改都會(huì)同步。比如下面的示例:
<div id="app">
? ? ? ? <my-component></my-component>
? ? ? ? <my-component></my-component>
? ? ? ? <my-component></my-component>
</div>
<script>
? ? var data={
? ? counter:0
? ? }
? ? Vue.component('my-component',{
? ? ? ? template:'<button @click="counter++">{{counter}}</template>
? ? ? ? data:function(){
? ? ? ? ? ? return data;
? ? ? ? }
? ? });
? ? var app = new Vue({
? ? ? ? el:'#app'
? ? })
? ? 組件使用了三次,但是點(diǎn)擊任意一個(gè)<button>,3個(gè)數(shù)字都會(huì)加1,那是因?yàn)榻M件的data引入的是外部的對(duì)象,這肯定不是我們期望的結(jié)果,所以給組件返回一個(gè)新的data對(duì)象來(lái)獨(dú)立,示例代碼如下:
……
<script>
? ? Vue.component('my-component',{
? ? template:'<button @click="counter++">{{counter}}</button>',
? ? data:function(){
? ? ? ? return {
? ? ? ? ? ? counter:0
? ? ? ? ? ? }
? ? ? ? }
? ? });
……
這樣,點(diǎn)擊三個(gè)按鈕就互不影響了,完全達(dá)到復(fù)用的目的。
7.2使用props傳遞數(shù)據(jù)
7.2.1基本用法
? ? ? ? 組件不僅僅是要把模板 的內(nèi)容進(jìn)行復(fù)用,更重要的是組件間要進(jìn)行通信。通常父組件的模板中包含子組件,父組件要正向地向子組件傳遞數(shù)據(jù)或參數(shù),子組件接收到后根據(jù)參數(shù)的不同來(lái)渲染不同的內(nèi)容或渲染操作。這個(gè)正向傳遞數(shù)據(jù)的過(guò)程就是通過(guò)props來(lái)實(shí)現(xiàn)的。
? ? ? ? 在組件中,使用選項(xiàng)props來(lái)聲明需要從父級(jí)接受的數(shù)據(jù),props的值可以是兩種,一種是字符串?dāng)?shù)組,一種是對(duì)象,本小節(jié)先介紹數(shù)組的用法。比如我們構(gòu)造一個(gè)數(shù)組,接收一個(gè)來(lái)自父級(jí)的數(shù)據(jù)message,并把它在組件模板中渲染,示例代碼如下:
<div id="app">
? ? <my-component message="來(lái)自父組件的數(shù)據(jù)"></my-component>
</div>
<script>
? ? Vue.component.('my-compnent',{
? ? ? ? pros:['message'],
? ? ? ? template:'<div>{{message}}</div>'
});
var app = new Vue({
? ? ? ? el:'#app'
})
渲染后的結(jié)果為:
<div id="app">
? ? <div>來(lái)自父組件的數(shù)據(jù)</div>?
</div>
? ? ? ? props中聲明的數(shù)據(jù)與組件data函數(shù)return的數(shù)據(jù)主要區(qū)別就是props的來(lái)自父級(jí),而data中的是組件自己的數(shù)據(jù),作用域是組件本身,這兩種數(shù)據(jù)都可以在模板template及計(jì)算屬性computed和方法methods中使用。上例的數(shù)據(jù)message就是通過(guò)props從父級(jí)傳遞過(guò)來(lái)的,在組件的自定義便簽上直接寫(xiě)該props的名稱(chēng),如果要傳遞多個(gè)數(shù)據(jù),在props數(shù)組中添加項(xiàng)即可。
? ? ? ? 由于HTML特性不區(qū)分大小寫(xiě),當(dāng)使用DOM模板時(shí),駝峰命名(camelCase)的props名稱(chēng)要轉(zhuǎn)為短橫分割命名(kebab-case),例如:
<div id="app">
? ? <my-component warning-text="提示信息"></my-component>
</div>
<script>
? ? Vue.Component('my-component',{
? ? props:['warningText'],
? ? template:'<div>{{warningText}}</div>'
});
? ? var app = new Vue({
? ? ? ? el:'#app'
})
</script>
? ? ? ? 有時(shí)候,傳遞的數(shù)據(jù)并不是直接寫(xiě)死的,而是來(lái)自父級(jí)的動(dòng)態(tài)數(shù)據(jù),這時(shí)可以使用命令v-bind來(lái)自動(dòng)態(tài)的綁定props的值,當(dāng)父組件的數(shù)據(jù)變化時(shí),也會(huì)傳遞給子組件,示例代碼如下:
<div? id="app">
? ? <input type="text" v-model="parentMessage">
? ? <my-component? :message="parentMessage"></my-component>
</div>
<script>
? ? Vue.component('my-component',{
? ? props:['message'],
? ? template:'<div>{{message}}</div>'
});
? ? var app = new Vue({
? ? ? ? el:'#app',
? ? ? ? data:{
? ? ? ? parentMessage:''
? ? }
})
</script>
? ? ? ? 這里用v-model綁定了父級(jí)的數(shù)據(jù)parentMessage,當(dāng)通過(guò)輸入框任意輸入時(shí),子組件接收到的props:"message"也會(huì)實(shí)時(shí)響應(yīng),并更新組件模板。
注意:如果你要直接傳遞數(shù)字、布爾值、數(shù)組、對(duì)象,而且不使用v-bind,傳遞的僅僅是字符串,嘗試下面的示例來(lái)對(duì)比:
<div id="app">
? ? ? ? <my-component message="[1,2,3]"></my-component>
? ? ? ? <my-component :message="[1,2,3]"></my-component>
</div>
<script>
? ? Vue.component('my-component',{
? ? ? ? props:['message'],
? ? ? ? template:'<div>{{message.length}}</div>'
});
var app = new Vue({
? ? ? ? el:‘#app’
})
</script>
同一個(gè)組件使用了兩次,區(qū)別僅僅是第二個(gè)使用的是v-bind,渲染后的結(jié)果,第一個(gè)是7,第二個(gè)才是數(shù)組的長(zhǎng)度3.
7.2.2單向數(shù)據(jù)流
? ? ? ? Vue2.x與Vu1.x比較大的一個(gè)區(qū)別就是,Vue2.x通過(guò)props傳遞數(shù)據(jù)是單向的了,也就是父組件數(shù)據(jù)變化時(shí)會(huì)傳遞給子組件,但是反過(guò)來(lái)不行。而在Vue1.x里提供了.sync修飾符來(lái)支持雙向綁定,之所以這樣設(shè)計(jì),是盡可能將父子組件解耦,避免子組件無(wú)意中修改了父組件的狀態(tài)。
? ? ? ? 業(yè)務(wù)中會(huì)經(jīng)常遇到兩種需要改變props的情況,一種是父組件傳遞初始值進(jìn)來(lái),子組件將它作為初始值保存起來(lái),在自己的作用域下可以隨意使用和修改,這種情況可以在組件data內(nèi)再聲明一個(gè)數(shù)據(jù),引入父組件的prop,示例代碼如下:
<div id="app">
? ? <my-component :init-count="1"></my-component>
</div>
<script>
? ? Vue.component('my-component',{
? ? ? ? props:['initCount'],
? ? ? ? template:'<div>{{count}}</div>',
? ? ? ? data:function(){
? ? ? ? ? ? return{
? ? ? ? ? ? ? ? count:this.initCount
? ? ? ? ? ? }
? ? ? ? }
? ? });
var app = new Vue({
? ? el:'#app'
})
</script>
? ? ? ? 組件中聲明了數(shù)據(jù)count,它在組件初始化時(shí)會(huì)獲取來(lái)自父組件的initCount,之后就與之無(wú)關(guān)了,只用維護(hù)count,這樣就可以避免直接操作initCount。
? ? ? ? 另一種情況就是prop作為需要被轉(zhuǎn)變的原始值傳入,這種情況使用計(jì)算屬性就可以了,示例代碼如下:
<div id="app">
? ? ? ? <my-component? :width="100"></my-componnet>
</div>
<script>
? ? Vue.component('my-component',{
? ? ? ? prosp:['width'],
? ? ? ? template:'<div :style="style">組件內(nèi)容</div>',
? ? ? ? data:{
? ? ? ? style:function(){
? ? ? ? ? ? return :{
? ? ? ? ? ? ? ? ? ? width:this.width+'px'
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? });
……
</script>
? ? ? ? 因?yàn)橛肅SS傳遞寬度要帶單位px,但是每次都寫(xiě)太麻煩,而且數(shù)值計(jì)算一般是不帶單位的,所以統(tǒng)一在組件內(nèi)使用計(jì)算屬性就可以了。
注意:在JavaScript中對(duì)象和數(shù)組是引用類(lèi)型,指向同一個(gè)內(nèi)存空間,所以props是對(duì)象和數(shù)組時(shí),在子組件內(nèi)改變是會(huì)影響父組件的。
7.2.3數(shù)據(jù)驗(yàn)證
? ? ? ? 我們上面所介紹的props選項(xiàng)的值都是一個(gè)數(shù)組,一開(kāi)始也介紹過(guò),除了數(shù)組外,還可以是對(duì)象,當(dāng)prop需要驗(yàn)證時(shí),就需要對(duì)象寫(xiě)法。
? ? ? ? 一般當(dāng)你的組件需要提供給別人使用時(shí),推薦都進(jìn)行數(shù)據(jù)驗(yàn)證,比如某個(gè)數(shù)據(jù)必須是數(shù)字類(lèi)型,如果傳入字符串,就會(huì)在控制臺(tái)彈出警告。
? ? ? ? 以下是幾個(gè)prop的示例:
Vue.component('my-component',{
? ? props:{
? ? ? ? //必須是數(shù)字類(lèi)型
? ? ? ? propA:Number,
? ? ? ? //必須是字符串或數(shù)字類(lèi)型
? ? ? ? propB:[String,Number],
? ? ? ? //布爾值,如果沒(méi)有定義,默認(rèn)值就是true
? ? ? ? propC:{
? ? ? ? ? ? type:Boolean,
? ? ? ? ? ? default:true
? ? ? ? },
? ? ? ? //數(shù)字,而且是必傳
? ? ? ? propD:{
? ? ? ? type:Number,
? ? ? ? required:true
? ? ? ? },
? ? ? ? //如果是數(shù)組或?qū)ο螅J(rèn)值必須是一個(gè)函數(shù)來(lái)返回
? ? ? ? propE:{
? ? ? ? type:Array,
? ? ? ? default:function(){
? ? ? ? ? ? ? ? return [];
? ? ? ? ? ? }
? ? ? ? },
? ? ? ? //自定義一個(gè)驗(yàn)證函數(shù)
? ? ? ? propF:{
? ? ? ? validator:function(value){
? ? ? ? ? ? return value>10
? ? ? ? ? ? }
? ? ? ? }
? ? }
});
驗(yàn)證的type類(lèi)型必須是:
? String
? Number
? Boolean
? Object
? Array
? Function
? ? ? ? type也可以是一個(gè)自定義構(gòu)造器,使用instanceof檢測(cè)。
? ? ? ? 當(dāng)prop驗(yàn)證失敗時(shí),在開(kāi)發(fā)版本下會(huì)在控制臺(tái)拋出一條警告。
7.3組件通信
? ? ? ? 我們已經(jīng)知道,從父組件向子組件通信,通過(guò)props傳遞數(shù)據(jù)就可以了,但Vue組件通信的場(chǎng)景不止有這一種,歸納起來(lái),組件關(guān)系可分為父子組件通信、兄弟組件通信、跨級(jí)組件通信。本節(jié)將介紹各種組件之間通信的方法。
7.3.1自定義事件
? ? ? ? 當(dāng)子組件需要向父組件傳遞數(shù)據(jù)時(shí),就要用到自定義事件。我們?cè)诮榻B指令v-on時(shí)有提到,v-on除了監(jiān)聽(tīng)DOM事件外,還可以用于組件之間的自定義事件。
? ? ? ? 如果你了解JavaScript的設(shè)計(jì)模式--觀察者模式,一定知道dispatchEvent和addEventListener這兩個(gè)方法。Vue組件也有與之類(lèi)似的一套模式,子組件用$emit()來(lái)觸發(fā)事件,父組件用$on()來(lái)監(jiān)聽(tīng)子組件的事件。
? ? ? ? 父組件也可以直接在子組件的自定義標(biāo)簽上使用v-on來(lái)監(jiān)聽(tīng)子組件觸發(fā)的自定義事件,示例代碼如下:
<div id="app">
? ? ? ? <p>總數(shù):{{total}}</p>
? ? ? ? <my-component?
? ? ? ? ? ? ? ? ? ? @increase="handleGetTotal"?
? ? ? ? ? ? ? ? ? ? @reduce="handleGetTotal"></my-component>
</div>
<script>
? ? Vue.component('my-component',{
? ? ? ? ? template:'\
? ? ? ? ? ? ? ? <div>
? ? ? ? ? ? ? ? ? ? ? ? <button @increase="handleIncrease">+1</button>
? ? ? ? ? ? ? ? ? ? ? ? <button @reduce="handleReducee">-1</button>
? ? ? ? ? ? ? ? </div>',
? ? ? ? ? ? data:function(){
? ? ? ? ? ? ? ? return{
? ? ? ? ? ? ? ? ? ? counter:0
? ? ? ? ? ? ? ? }
? ? ? ? ? ? },
? ? ? ? ? ? methods:{
? ? ? ? ? ? ? ? handleIncrease:function(){
? ? ? ? ? ? ? ? ? ? ? ? this.counter++;
? ? ? ? ? ? ? ? ? ? ? ? this.$emit('increase',this.counter);
? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? handleReduce:function(){
? ? ? ? ? ? ? ? ? ? ? ? this.counter--;
? ? ? ? ? ? ? ? ? ? ? ? this.$emit('reduce',this.counter);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? });
var app = new? Vue({
? ? el:'#app',
? ? data:{
? ? ? ? total:0
? ? },
? ? methods:{
? ? handleGetTotal:function(total){
? ? ? ? this.total = total;
? ? ? ? }
? ? }
})
</script>
? ? ? ? 上面示例中,子組件有兩個(gè)按鈕,分別實(shí)現(xiàn)加一和減一的效果,在改變組件的data"counter"后,通過(guò)$.emit()再把它傳遞給父組件,父組件用v-on:increase和v-on:reduce,$emit()方法的第一個(gè)參數(shù)是自定義事件的名稱(chēng),例如示例的increase和reduce后面的參數(shù)都是要傳遞的數(shù)據(jù),可以不填或填寫(xiě)多個(gè)。
? ? ? ? 除了用v-on在組件上監(jiān)聽(tīng)自定義事件,也可以監(jiān)聽(tīng)DOM事件,這時(shí)可以用.native修飾符表示監(jiān)聽(tīng)的是一個(gè)原生事件 ,監(jiān)聽(tīng)的是該組件的根元素,示例代碼如下:
<my-component v-on:click.native="handleClick"></my-component>
7.3.2使用v-model
? ? ? ? Vue2.x可以在自定義組件上使用v-model指令,我們先看一個(gè)示例:
<div id="app">
? ? <p>總數(shù):{{ total }}</p>
? ? <my-component v-model="total"></my-component>
</div>
<script>
? ? Vue.component('my-component',{
? ? ? ? template:'<button @click="handleClick">+1</button>',
? ? ? ? data:function(){
? ? ? ? return {
? ? ? ? ? ? counter:0
? ? ? ? ? ? }
? ? ? ? },
? ? ? ? methods:{
? ? ? ? ? ? handleClick:function(){
? ? ? ? ? ? ? ? this.counter++;
? ? ? ? ? ? ? ? this.$emit('input',this.counter);
? ? ? ? ? ? }
? ? ? ? }
? ? });
var app = new Vue({
? ? el:'#app',
? ? data:{
? ? total:0
? ? ? ? }
})
</script>
? ? ? ? 仍然是點(diǎn)擊按鈕加一的效果,不過(guò)這次組件$emit()的事件名是特殊的input,在使用組件的父級(jí),并沒(méi)有在<my-component>上使用@input="handler",而是直接用了v-model綁定一個(gè)數(shù)據(jù)total。這也可以稱(chēng)作一個(gè)語(yǔ)法糖,因?yàn)樯厦娴氖纠梢蚤g接地用自定義事件來(lái)實(shí)現(xiàn):
……
<my-component @input="handleGetTotal|"></my-component>
……
var app =new Vue({
? ? el:'#app',
? ? data:{totoal:0},
? ? methods:{
? ? ? ? handleGetTotal:function(total){
? ? ? ? this.total=total;
? ? ? ? }
? ? }
})
v-model還可以用來(lái)創(chuàng)建自定義的表單輸入組件,進(jìn)行數(shù)據(jù)雙向綁定,例如:
<div id="app">
? ? <p>總數(shù):{{total}}</p>
? ? <my-component v-model="total"></my-component>
? ? <button @click="handleReduce">-1</button>
</div>
<script>
? ? Vue.component('my-component',{
? ? props:{'value'},
? ? template:'<input? :value="value" @input="updateValue">',
? ? methods:{
? ? ? ? updateValue:function(event){
? ? ? ? ? ? this.$emit('input',event.target.value);
? ? ? ? }
? ? }
});
var app = new Vue({
? ? el:'#app',
? ? data:{
? ? ? ? total:0
? ? },
? ? methods:{
? ? ? ? handleReduce:function(){
? ? ? ? ? ? this.total--;
? ? ? ? }
? ? }
})
</script>
實(shí)現(xiàn)這樣一個(gè)具有雙向綁定的v-model組件要滿足下面兩個(gè)要求:
接收一個(gè)value屬性
在有新的value時(shí)觸發(fā)input事件。
7.3.3非父子組件通信
? ? ? ? 在實(shí)際業(yè)務(wù)中,除了父子組件通信外,還有很多非父子組件通信的場(chǎng)景,非父子組件一般有兩種,兄弟組件和跨多級(jí)組件,為了更加徹底地了解Vue.js2.x中的通信方法,我們先來(lái)看一下在Vue.js1.x中是如何實(shí)現(xiàn)的,這樣便于我們了解Vue.js的設(shè)計(jì)思想。
? ? ? ? 在Vue.js1.x中,除了$emit()方法外,還提供了$dispatch()和$broadcast()這兩個(gè)方法。$dispatch()用于向上級(jí)派發(fā)事件,只要是它的父級(jí)(一級(jí)或多級(jí)以上),都可以在Vue實(shí)例的events選項(xiàng)內(nèi)接收,實(shí)例代碼如下:
<!--注意:該示例需使用Vue.js1.x的版本-->
<div id="app">
? ? {{ message }}
? ? <my-component></my-component>
</div>
<script>
? ? Vue.component(my-component',{
? ? ? ? template:'<buttonj @click="handleDispatch">派發(fā)事件</button>',
? ? ? ? methods:{
? ? ? ? ? ? handleDispatch:function(){
? ? ? ? ? ? ? ? this.$dispatch('on-message','來(lái)自內(nèi)部組件的數(shù)據(jù)');
? ? ? ? ? ? }
? ? ? ? }
? ? }');
? ? var app = new Vue({
? ? ? ? el:'#app',
? ? ? ? data:{
? ? ? ? message:''
? ? ? ? },
? ? ? ? event:{
? ? ? ? "on-message":function(msg){
? ? ? ? ? ? this.message = msg ;
? ? ? ? }
? ? }
})
</script>
? ? ? ? 同理,$broadcast()是由上級(jí)向下級(jí)廣播事件的,用法完全一致,只是方向相反。
? ? ? ? 這兩種方法一旦發(fā)出事件后,任何組件都是可以接收到的,就近原則,而且會(huì)在第一次接收到后停止冒泡,除非返回true。
? ? ? ? 這兩個(gè)方法雖然看起來(lái)很好用,但是在Vue.js2.x中都廢棄了,因?yàn)榛诮M件樹(shù)結(jié)構(gòu)的事件流方式讓人難以理解,并且在組件結(jié)構(gòu)擴(kuò)展的過(guò)程中會(huì)變得越來(lái)越脆弱,并且不能解決兄弟組件通信的問(wèn)題。
? ? ? ? 在Vue.js2.x中,推薦使用一個(gè)空的Vue實(shí)例作為中央事件總線(bus),也就是一個(gè)中介。為了更形象的了解它,我們舉一個(gè)生活中的例子:
? ? ? ? 比如你需要租房子,你可能會(huì)找房產(chǎn)中介來(lái)登記你的需求,然后中介把你的信息發(fā)給滿足要求的出租者,出租者再把報(bào)價(jià)和看房的時(shí)間告訴中介,由中介再轉(zhuǎn)達(dá)給你,整個(gè)過(guò)程中,買(mǎi)家和賣(mài)家并沒(méi)有任何交流,都是通過(guò)中介來(lái)傳話的。
? ? ? ? 或者你最近可能要換房了,你會(huì)找房產(chǎn)中介登記你的信息,訂閱與你找房需求相關(guān)的資訊,一旦有符合你的房子出現(xiàn)時(shí),中介會(huì)通知你,并傳達(dá)你房子的具體信息。
? ? ? ? 這兩個(gè)例子中,你和出租者擔(dān)任的就是兩個(gè)跨級(jí)的組件,而房產(chǎn)中介就是這個(gè)中央事件總線(bus)。比如下面的示例代碼:
<div id="app">
? ? ? ? {{ message }}
? ? ? ? <component-a></component-a>
</div>
<script>
? ? ? ? var bus = new Vue({});
? ? ? ? Vue.component('component-a',{
? ? ? ? ? ? template:'<button @click="handleEvent">傳遞事件</button>',
? ? ? ? ? ? methods:{
? ? ? ? ? ? handleEvent:function(){
? ? ? ? ? ? ? ? bus.$emit('on-message','來(lái)自組件component-a的內(nèi)容');
? ? ? ? ? ? }
? ? ? ? }
? ? });
? ? var app = new Vue({
? ? ? ? el:'#app',
? ? ? ? data:{
? ? ? ? message:''
? ? ? ? },
? ? ? ? mounted:function(){
? ? ? ? var _this = this;
? ? //在實(shí)例初始化時(shí),監(jiān)聽(tīng)來(lái)自bus實(shí)例的事件
? ? bus.$on('on-message',function(msg){
? ? ? ? _this.message = msg;
? ? ? ? ? });
? ? ? ? }
? ? })
</script>
? ? ? ? 首先創(chuàng)建了一個(gè)名為bus的空Vue實(shí)例,里面沒(méi)有任何內(nèi)容;然后全局定義了組件component-a;最后創(chuàng)建Vue實(shí)例app,在app初始化時(shí),也就是在生命周期mounted鉤子函數(shù)里監(jiān)聽(tīng)了來(lái)組bus的事件on-message,而在組件component-a中,點(diǎn)擊按鈕會(huì)通過(guò)bus把事件on-message發(fā)出去,此時(shí)app就會(huì)接受到來(lái)自bus的事件,進(jìn)而在回調(diào)里完成自己的業(yè)務(wù)邏輯。
? ? ? ? 這種方法巧妙而輕量地實(shí)現(xiàn)了任何組件間的通信,包括父子、兄弟、跨級(jí),而且Vue1.x和Vue2.x都適用。如果深入使用,可以擴(kuò)展bus實(shí)例,給它添加data、methods、computed等選項(xiàng),比如用戶登錄的昵稱(chēng)、性別、郵箱等,還有用戶的授權(quán)token等。只需在初始化時(shí)讓bus獲取一次,任何時(shí)間、任何組件就可以從中直接使用了,在單頁(yè)面富應(yīng)用(SPA)中會(huì)很實(shí)用,我們會(huì)在進(jìn)階篇里逐步介紹這些內(nèi)容。
? ? ? ? 當(dāng)你的項(xiàng)目比較大,有很多的小伙伴參與開(kāi)發(fā)時(shí),也可以選擇更好的狀態(tài)管理解決方案vuex,在進(jìn)階篇里會(huì)詳細(xì)介紹關(guān)于它的用法。
? ? ? ? 除了中央事件總線bus外,還有兩種方法可以實(shí)現(xiàn)組件間通信:父鏈和子組件索引。
父鏈
? ? ? ? 在子組件中,使用this.$parent可以直接訪問(wèn)該組件的父實(shí)例或組件,父組件也可以通過(guò)this.$children訪問(wèn)它所有的子組件,而且可以遞歸向上或向下無(wú)限訪問(wèn),直到根實(shí)例或最內(nèi)層的組件。示例代碼如下:
<div id="app">
? ? {{ message }}
? ? <component-a></component-a>
</div>
<script>
? ? Vue.component('component-a',{
? ? ? ? template:'<button @click="handleEvent">通過(guò)父鏈直接修改數(shù)據(jù)</button>',
? ? ? ? methods:{
? ? ? ? handleEvent:function(){
? ? ? ? ? ? ? ? //訪問(wèn)到父鏈后,可以做任何操作,比如直接修改數(shù)據(jù)
? ? ? ? ? ? ? ? this.$parent.message = '來(lái)自組件component-a的內(nèi)容';
? ? ? ? ? ? }
? ? ? ? }
? ? });
? ? var app =new Vue({
? ? el:'#app',
? ? data:{
? ? ? ? message:''
? ? }
})
</script>
? ? ? ? 盡管Vue允許這樣操作,但在業(yè)務(wù)中,子組件應(yīng)該盡可能地避免依賴父組件的數(shù)據(jù),更不應(yīng)該主動(dòng)的去修改它的數(shù)據(jù),因?yàn)檫@樣使得父子組件緊耦合,只看父組件,很難理解父組件的狀態(tài),因?yàn)樗赡鼙蝗我饨M件修改,理想情況下,只有組件自己能修改它的狀態(tài)。父子組件最好還是通過(guò)props和$emit來(lái)通信。
子組件索引
? ? ? ? 當(dāng)子組件較多時(shí),通過(guò)this.$children來(lái)一一遍歷出我們需要的一個(gè)組件實(shí)例是比較困難的,尤其是組件動(dòng)態(tài)渲染時(shí),它們的序列是不固定的。Vue提供了子組件索引的方法,用特殊的屬性ref來(lái)為子組件指定一個(gè)索引名稱(chēng),示例代碼如下:
<div id="app">
? ? <button @click="handleRef">通過(guò)ref獲取子組件實(shí)例</button>
? ? <component-a ref="comA"></component-a>
</div>
<script>
? ? ? ? Vue.component('component-a',{
? ? ? ? template:'<div>子組件</div>',
? ? ? ? data:function(){
? ? ? ? ? ? message:'子組件內(nèi)容'
? ? },
? ? var app =new Vue({
? ? ? ? el:'#app',
? ? ? ? methods:{
? ? ? ? ? ? handleRef:fucntion(){
? ? ? ? ? ? ? ? var msg = this.$refs.comA.message;
? ? ? ? ? ? ? ? console.log(msg);
? ? ? ? ? ? }
? ? ? ? }
? ? })
});
</script>
? ? ? ? 在父組件模板中,子組件標(biāo)簽上使用ref指定一個(gè)名稱(chēng),并在父組件內(nèi)通過(guò)this.$refs來(lái)訪問(wèn)指定名稱(chēng)的子組件。
注意:$refs只在組件渲染完成后才填充,并且它是非響應(yīng)式的。它僅僅作為一個(gè)直接訪問(wèn)子組件的應(yīng)急方案,應(yīng)當(dāng)避免在模板或計(jì)算屬性中使用$refs.
? ? ? ? 與Vue1.x不同的是,Vue2.x將v-el和v-ref合并為了ref,Vue會(huì)自動(dòng)去判斷是普通標(biāo)簽還是組件。可以嘗試補(bǔ)全下面的代碼,分別打印出兩個(gè)ref看看都是什么:
<div id="app">
? ? <p ref="p">內(nèi)容</p>
? ? <child-component ref="child"></child-component>
</div>
7.4使用slot分發(fā)內(nèi)容
7.4.1什么是slot
? ? ? ? 看一個(gè)常規(guī)網(wǎng)站布局,一般由一級(jí)導(dǎo)航、二級(jí)導(dǎo)航、左側(cè)列表、正文以及底部版權(quán)信息5個(gè)模塊組成,如果要講它們都組件化,這個(gè)結(jié)構(gòu)可能會(huì)是:
<app>
? ? <menu-main></menu-main>
? ? <menu-sub></menu-sub>
? ? <div class="container">
? ? ? ? <menu-left<</menu-left>
? ? ? ? <container></container>
? ? </div>
? ? <app-footer></app-footer>
</app>? ?
? ? ? ? 當(dāng)需要讓組件組合使用,混合父組件的內(nèi)容與子組件的模板時(shí),就會(huì)用到slot,這個(gè)過(guò)程叫做內(nèi)容分發(fā)(transclusion)。以<app>為例,它有兩個(gè)特點(diǎn):
<app>組件不知道它的掛載點(diǎn)會(huì)有什么內(nèi)容。掛載點(diǎn)的內(nèi)容是由<app>的父組件決定的。
<app>組件很可能有它自己的模板。
? ? ? ? props傳遞數(shù)據(jù),enents觸發(fā)事件和slot內(nèi)容分發(fā)就構(gòu)成了Vue組件的三個(gè)API來(lái)源,再?gòu)?fù)雜的組件也是由這三部分構(gòu)成的。
7.4.2作用域
? ? ? ? 正式介紹slot前,需要先指定一個(gè)概念:編譯的作用域。比如父組件中有如下模板:
<child-component>
? ? {{ message }}
</chid-component>
? ? ? 這里的message就是一個(gè)slot,但是它綁定的是父組件的數(shù)據(jù),而不是組件<chid-component>的數(shù)據(jù)。
? ? ? ? 父組件模板的內(nèi)容是在父組件作用域內(nèi)編譯,子組件模板的內(nèi)容是在子組件作用域內(nèi)編譯。例如下面的代碼 示例:
<div id="app">
<child-component v-show="showChild:></child-component>
</div>
<script>
Vue.component('child-component',{
template:'<div>子組件</div>'
});
var app = new Vue({
el:'#app",
data:{
showChild:true
}
})
</script>
這里的狀態(tài)showChild綁定的是父組件的數(shù)據(jù),如果想在子組件上綁定,那應(yīng)該是:
<div id="app">
<child-component></child-component>
</div>
<script>
Vue.component('component-a',{
template:'<div v-show="showChild">子組件</div>',
data:function(){
return {
showChild:true
}
}
});
var app =new Vue({
el:'#app'
})
</script>
因此,slot分發(fā)的內(nèi)容,作用域是在父組件上的。
7.4.3 slot用法
單個(gè)slot
在子組件內(nèi)使用特殊的<slot>元素就可以為這個(gè)子組件開(kāi)啟一個(gè)slot(插槽),在父組件模板里,插入子組件標(biāo)簽內(nèi)的所有內(nèi)容將替代子組件的<slot>標(biāo)簽以及它的內(nèi)容。示例代碼如下:
<div id="app">
<child-component>
<p>分發(fā)的內(nèi)容</p>
<p>更多分發(fā)的內(nèi)容</p>
</child-component>
</div>
<script>
Vue.component('child-component',{
template:'\
<div>\
<slot>\
<p>如果父組件沒(méi)有插入內(nèi)容,我將作為默認(rèn)出現(xiàn)</p>\
</slot>\
</div>'
});
var app = new Vue({
el:'#app'
})
</script>
具名Slot
給<slot>元素指定一個(gè)name后可以分發(fā)多個(gè)內(nèi)容,具名Slot可以與單個(gè)Slot共存,例如下面的示例:
<div id="app">
<child-component>
<h2 slot="header">標(biāo)題</h2>
<p>正文內(nèi)容</p>
<p>更多的正文內(nèi)容</p>
<div slot="footer">底部信息</div>
</child-component>
</div>
<script>
Vue.component('child-component',{
template:'\
<div class ="container">\
<slot name="header"></slot>\
</div>\
<div class="main">\
<slot></slot>\
</div>\
<div class="footer">\
<slot name="footer"></slot>\
</div>\
</div>'
});
var app = new Vue({
el:'#app'
})
</script>
子組件內(nèi)聲明了3個(gè)<slot>元素,其中在<div class="main">內(nèi)的<slot>沒(méi)有使用name屬性,它將作為默認(rèn)slot出現(xiàn),父組件沒(méi)有使用slot特性的元素與內(nèi)容都將出現(xiàn)在這里。
如果沒(méi)有指定默認(rèn)的匿名slot,父組件內(nèi)多余的內(nèi)容片段都將被拋棄。
上例最終渲染后的結(jié)果為:
<div id="app">
? ? <div class="container">
????????<div class="header">
? ? ? ? ? ? ? ? <h2>標(biāo)題</h2>
? ? ? ? </div>
? ? ? ? <div class="main">
? ? ? ? ? ? <p>正文內(nèi)容</p>
? ? ? ? ? ? <p>更多的正文內(nèi)容</p>
? ? ? ? </div>
? ? ? ? <div class="footer">
? ? ? ? ? ? <div>底部信息</div>
????????</div>
? ? </div>
</div>
在組合使用747組件時(shí),內(nèi)容分發(fā)API至關(guān)重要。
7.4.4作用域插槽
????????作用域插槽是一種特殊的slot,使用一個(gè)可以復(fù)用的模板替換已渲染元素。概念比較難理解,我們先看一個(gè)簡(jiǎn)單的示例來(lái)了解它的基本用法。示例代碼如下:
<div id="app">
? ? <child-component>
? ? ? ? <template scope="props">
? ? ? ? ? ? <p>來(lái)自父組件的內(nèi)容</p>
? ? ? ? ? ? <p>{{ props.msg}}</p>
? ? ? ? </template>
? ? </child-component>
</div>
<script>
? ? Vue.component('child-component',{
? ? ? ? template:'\
? ? ? ? ? ? <div class="container">\
? ? ? ? ? ? ? ? <slot msg="來(lái)自子組件的內(nèi)容"></slot>\
? ? ? ? ? ? </div>'
});
var app = new Vue({
? ? el:'#app'
})
</script>
? ? ? ? 觀察子組件的模板,在<slot>元素上由一個(gè)類(lèi)似props傳遞數(shù)據(jù)給組件的寫(xiě)法msg="xxx",將數(shù)據(jù)傳到了插槽。父組件中使用了<template>元素,而且擁有一個(gè)scope="props"的特性,這里的props只是一個(gè)臨時(shí)變量,就像v-for="item in items"里面的item一樣。template內(nèi)可以通過(guò)臨時(shí)變量props訪問(wèn)來(lái)自子組件插槽的數(shù)據(jù)msg。
將上面的示例渲染后的最終結(jié)果為:
<div id="app">
<div class="container">
? ? <p>來(lái)自父組件的內(nèi)容</p>
? ? <p>來(lái)自子組件的內(nèi)容</p>
</div>
</div>
? ? ? ? 作用域插槽更具代表性的用例是列表組件,允許組件自定義應(yīng)該如何渲染列表每一項(xiàng)。示例代碼如下:
<div id="app">
? ? <my-list :book="books">
? ? ? ? <!--作用域的插槽也可以是具名的slot-->
? ? ? ? ? ? <template slot="book" scope="props">
? ? ? ? ? ? ? ? <li>{{ props.bookName}}</li>
? ? ? ? ? ? </template>
? ? </my-list>
</div>
<script>
Vue.component('my-list',{
? ? props:{
? ? ? ? books:{
? ? ? ? ? ? type:Array,
? ? ? ? ? ? default:function(){
? ? ? ? ? ? ? ? return []'
????????????}
????????}
????},
? ? template:'\
? ? ? ? <ul>\
? ? ? ? ? ? <slot name="book" v-for="book in books" :book-name="book.name"></slot>\
? ? ? ? </ul>'
});
var app = new Vue({
? ? el:'#app',
? ? data:{
? ? ? ? books:[
????????????{name: '《Vue.js實(shí)戰(zhàn)》'},
????????????{name: '《Vue.js實(shí)戰(zhàn)之六個(gè)周》'},
????????????{name: '《Vue.js實(shí)戰(zhàn)之簡(jiǎn)書(shū)六個(gè)周》'},
????????]
????}
})
</script>
????????子組件my-list接收一個(gè)來(lái)自父級(jí)的props數(shù)組books,并且將它在name為book的slot上使用v-for指令循環(huán),同時(shí)暴露一個(gè)變量bookName。
????????上示例注意是為了介紹作用域插槽的用法。
7.4.5訪問(wèn)slot
? ? ? ? 在Vue.js1.x中,想要獲取某個(gè)slot是比較麻煩的,需要用v-el間接獲取。而Vue.js2.x提供了用來(lái)訪問(wèn)被slot分發(fā)的內(nèi)容的方法$slots,請(qǐng)看下面的示例:
<div id="app">
? ? <child-component>
? ? ? ? <h2 slot="header">標(biāo)題</h2>
? ? ? ? <p>正文內(nèi)容</p>
? ? ? ? <p>更多的正文內(nèi)容</p>
? ? ? ? <div slot="footer">底部?jī)?nèi)容</div>
? ? </child-component>
</div>
<script>
? ? Vue.component('child-component',{
? ? ? ? template:'\
? ? ? ? <div class="container“>\
? ? ? ? ? ? <div class="header">\
? ? ? ? ? ? ? ? <slot name="header"></slot>\
? ? ? ? ? ? </div>\
? ? ? ? ? ? <div class="main">\
? ? ? ? ? ? ? ? ? ? <slot></slot>\
? ? ? ? ? ? </div>
? ? ? ? ? ? <div class="footer">\
? ? ? ? ? ? ? ? <slot name="footer"></slot>\
? ? ? ? ? ? </div>\
? ? ? ? </div>',
? ? methods:{
? ? ? ? var header = this.$slot.header;
? ? ? ? var main = this.$slot.default;
? ? ? ? var footer = this.$slot.footer;
? ? ? ? console.log(footer);
? ? ? ? console.log(footer[0].elm.innerHTML);
????};
? ? var app = new Vue({
? ? ? ? el:'#app'
????})
});
</script>
? ? ? ? 通過(guò)$slot可以訪問(wèn)某個(gè)具名slot,this.$slot.default包括了所有沒(méi)有被包含在具名slot中的節(jié)點(diǎn)。嘗試編寫(xiě)代碼,查看兩個(gè)console打印的內(nèi)容。
? ? ? ? $slot在業(yè)務(wù)中幾乎用不到,在用render函數(shù)(進(jìn)階篇中將介紹)創(chuàng)建組件時(shí)會(huì)比較有用,但注意還是用于獨(dú)立組件開(kāi)發(fā)中。
更多內(nèi)容,請(qǐng)?jiān)L問(wèn)的我的個(gè)人博客:[https://liugezhou.github.io/blog](https://liugezhou.github.io/blog).
您也可以關(guān)注我的個(gè)人公眾號(hào):【W(wǎng)akaka】