第七章:vue.js組件詳解Ⅰ(基礎篇)

? ? ? 組件(Component)是Vue.js最核心的功能,也是整個架構設計最精彩的地方,當然也是最難掌握的。本章將帶領你由淺入深地學習組件的全部內容,并通過幾個實戰項目熟練使用Vue組件。

7.1組件與復用

7.1.1為什么使用組件? ?

? ? ? ? 組建可以提高一些控件、JavaScript能力的復用,提高組件重用性,讓代碼可復用。我們先寫一個聊天頁面的,示例代碼如下:

<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="請輸入……"</i-input>

? ? ? ? ? ? </i-col>

? ? ? ? ? ? <i-col span="4">

? ? ? ? ? ? ? ? <i-button type="primary" icon="paper-airplane"發送</i-button>

? ? ? ? ? ? </i-col>

? ? ? ? </Row>

? ? </div>

</Card>

? ? ? ? 是不是很奇怪,有很多我們從未使用過的標簽,比如<Card>、<Row>、<i-col>、<i-input>、<i-button>等,而且整段代碼除了內聯的幾個樣式外,一句CSS代碼也沒有。

? ? ? ? 這些沒見過的自定義標簽就是組件,每個標簽代表一個組件,在任何時候使用Vue的地方都可以直接使用。接下來,我們就來看看組件的具體用法。

7.1.2組建用法

? ? ? ? 回顧創建Vue示例的方法,我們發現組件與之類似,需要注冊后才可以使用。注冊有全局注冊局部注冊兩種方式,全局注冊后,任何Vue實例都可以使用,全局注冊示例代碼如下:

? ? ? ? Vue.component('my-component',{

? ? ? ? ? ? //my-component就是注冊的組件自定義標簽名稱,推薦使用小寫加減號分隔符的形式命名

? ? });

? ? ? ? 要在父實例中使用這個組件,必須要在實例創建前注冊,之后就可以用<my-component>來使用組件,實例代碼如下:

? ? <div id="app">

? ? ? ? <my-component></my-component >

? ? <div>

? ? <script>

? ? ? Vue.component("my-component",{

? ? ? ? ? template:'<div>這里是組件內容</div>'

//template的DOM結構必須被一個元素包含,如果直接寫成“這里是組件的內容”,不帶"<div></div>是無法被渲染的“

? ? });

? ? var app = new Vue({

? ? ? ? el:'#app'

? ? })

<script>

? ? ? ? 在Vue實例中,使用components選項可以局部注冊組件,注冊后的組件只有在該實例作用域下有效。組件中也可也使用components選項來注冊組件,使組件可以嵌套。示例代碼如下:

? ? <div id="app">

? ? ? ? <my-component></my-component>

? ? </div>

? ? <script>

? ? ? var Child={

? ? ? ? template:'<div>局部注冊組件的內容</div>

? ? }

? ? ? ? var app = new Vue({

? ? ? ? el:'#app',

? ? ? ? compoments:{

? ? ? ? ? ? 'my-component':Child

? ? ? ? }

? ? });

? ? ? ? Vue組件的模板在某些情況下會受到HTML的影響,比如<table>內規定只允許是<tr>、<td>、<th>等這些表格元素,所以在<table>內直接使用組件是無效的。這種情況下,可以使用特殊的屬性來掛載組件,示例代碼如下:

? ? <div id="app>>

? ? ? ? <table>

? ? ? ? ? ? <tbody is="my-component"></tbody>

? ? ? ? </table>

? ? </div>

? ? <script>

? ? ? ? Vue.componnt("my-compone",{

? ? ? ? ? ? template:'<div>這里是組件里的內容</div>"

? ? });

? ? ? ? var app =new Vue({

? ? ? ? ? ? el:'#app'

? ? })

? ? </script>

? ? tips:如果使用的是字符串模板,是不受限制的,比如后面章節介紹的.VUE單文件用法等。

? ? 除了template選項外,組件中還可以像Vue實例那樣使用其它的選項,比如data、computed、methods。但是在使用data時,和實例稍有區別,data必須是函數,然后將數據return出去。例如:

<div id="app">

? ? <my-component></my-component>

</div>

<script>

? ? Vue.component('my-component',{

? ? ? ? template:'<div>message</div>',

? ? ? ? data:function(){

? ? ? ? ? ? return {

? ? ? ? ? ? ? ? message:'組件內容'

? ? ? ? }

? ? }

});

……

JavaScript對象是引用關系,所以如果return出的對象引用了外部的一個對象,那這個對象就是共享的,任何一部分修改都會同步。比如下面的示例:

<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'

? ? })

? ? 組件使用了三次,但是點擊任意一個<button>,3個數字都會加1,那是因為組件的data引入的是外部的對象,這肯定不是我們期望的結果,所以給組件返回一個新的data對象來獨立,示例代碼如下:

……

<script>

? ? Vue.component('my-component',{

? ? template:'<button @click="counter++">{{counter}}</button>',

? ? data:function(){

? ? ? ? return {

? ? ? ? ? ? counter:0

? ? ? ? ? ? }

? ? ? ? }

? ? });

……

這樣,點擊三個按鈕就互不影響了,完全達到復用的目的。


7.2使用props傳遞數據

7.2.1基本用法

? ? ? ? 組件不僅僅是要把模板 的內容進行復用,更重要的是組件間要進行通信。通常父組件的模板中包含子組件,父組件要正向地向子組件傳遞數據或參數,子組件接收到后根據參數的不同來渲染不同的內容或渲染操作。這個正向傳遞數據的過程就是通過props來實現的。

? ? ? ? 在組件中,使用選項props來聲明需要從父級接受的數據,props的值可以是兩種,一種是字符串數組,一種是對象,本小節先介紹數組的用法。比如我們構造一個數組,接收一個來自父級的數據message,并把它在組件模板中渲染,示例代碼如下:

<div id="app">

? ? <my-component message="來自父組件的數據"></my-component>

</div>

<script>

? ? Vue.component.('my-compnent',{

? ? ? ? pros:['message'],

? ? ? ? template:'<div>{{message}}</div>'

});

var app = new Vue({

? ? ? ? el:'#app'

})

渲染后的結果為:

<div id="app">

? ? <div>來自父組件的數據</div>?

</div>

? ? ? ? props中聲明的數據與組件data函數return的數據主要區別就是props的來自父級,而data中的是組件自己的數據,作用域是組件本身,這兩種數據都可以在模板template及計算屬性computed和方法methods中使用。上例的數據message就是通過props從父級傳遞過來的,在組件的自定義便簽上直接寫該props的名稱,如果要傳遞多個數據,在props數組中添加項即可。

? ? ? ? 由于HTML特性不區分大小寫,當使用DOM模板時,駝峰命名(camelCase)的props名稱要轉為短橫分割命名(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>

? ? ? ? 有時候,傳遞的數據并不是直接寫死的,而是來自父級的動態數據,這時可以使用命令v-bind來自動態的綁定props的值,當父組件的數據變化時,也會傳遞給子組件,示例代碼如下:

<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綁定了父級的數據parentMessage,當通過輸入框任意輸入時,子組件接收到的props:"message"也會實時響應,并更新組件模板。

注意:如果你要直接傳遞數字、布爾值、數組、對象,而且不使用v-bind,傳遞的僅僅是字符串,嘗試下面的示例來對比:

<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>

同一個組件使用了兩次,區別僅僅是第二個使用的是v-bind,渲染后的結果,第一個是7,第二個才是數組的長度3.

7.2.2單向數據流

? ? ? ? Vue2.x與Vu1.x比較大的一個區別就是,Vue2.x通過props傳遞數據是單向的了,也就是父組件數據變化時會傳遞給子組件,但是反過來不行。而在Vue1.x里提供了.sync修飾符來支持雙向綁定,之所以這樣設計,是盡可能將父子組件解耦,避免子組件無意中修改了父組件的狀態。

? ? ? ? 業務中會經常遇到兩種需要改變props的情況,一種是父組件傳遞初始值進來,子組件將它作為初始值保存起來,在自己的作用域下可以隨意使用和修改,這種情況可以在組件data內再聲明一個數據,引入父組件的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>

? ? ? ? 組件中聲明了數據count,它在組件初始化時會獲取來自父組件的initCount,之后就與之無關了,只用維護count,這樣就可以避免直接操作initCount。

? ? ? ? 另一種情況就是prop作為需要被轉變的原始值傳入,這種情況使用計算屬性就可以了,示例代碼如下:

<div id="app">

? ? ? ? <my-component? :width="100"></my-componnet>

</div>

<script>

? ? Vue.component('my-component',{

? ? ? ? prosp:['width'],

? ? ? ? template:'<div :style="style">組件內容</div>',

? ? ? ? data:{

? ? ? ? style:function(){

? ? ? ? ? ? return :{

? ? ? ? ? ? ? ? ? ? width:this.width+'px'

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? });

……

</script>

? ? ? ? 因為用CSS傳遞寬度要帶單位px,但是每次都寫太麻煩,而且數值計算一般是不帶單位的,所以統一在組件內使用計算屬性就可以了。

注意:在JavaScript中對象和數組是引用類型,指向同一個內存空間,所以props是對象和數組時,在子組件內改變是會影響父組件的。

7.2.3數據驗證

? ? ? ? 我們上面所介紹的props選項的值都是一個數組,一開始也介紹過,除了數組外,還可以是對象,當prop需要驗證時,就需要對象寫法。

? ? ? ? 一般當你的組件需要提供給別人使用時,推薦都進行數據驗證,比如某個數據必須是數字類型,如果傳入字符串,就會在控制臺彈出警告。

? ? ? ? 以下是幾個prop的示例:

Vue.component('my-component',{

? ? props:{

? ? ? ? //必須是數字類型

? ? ? ? propA:Number,

? ? ? ? //必須是字符串或數字類型

? ? ? ? propB:[String,Number],

? ? ? ? //布爾值,如果沒有定義,默認值就是true

? ? ? ? propC:{

? ? ? ? ? ? type:Boolean,

? ? ? ? ? ? default:true

? ? ? ? },

? ? ? ? //數字,而且是必傳

? ? ? ? propD:{

? ? ? ? type:Number,

? ? ? ? required:true

? ? ? ? },

? ? ? ? //如果是數組或對象,默認值必須是一個函數來返回

? ? ? ? propE:{

? ? ? ? type:Array,

? ? ? ? default:function(){

? ? ? ? ? ? ? ? return [];

? ? ? ? ? ? }

? ? ? ? },

? ? ? ? //自定義一個驗證函數

? ? ? ? propF:{

? ? ? ? validator:function(value){

? ? ? ? ? ? return value>10

? ? ? ? ? ? }

? ? ? ? }

? ? }

});

驗證的type類型必須是:

? String

? Number

? Boolean

? Object

? Array

? Function

? ? ? ? type也可以是一個自定義構造器,使用instanceof檢測。

? ? ? ? 當prop驗證失敗時,在開發版本下會在控制臺拋出一條警告。


7.3組件通信

? ? ? ? 我們已經知道,從父組件向子組件通信,通過props傳遞數據就可以了,但Vue組件通信的場景不止有這一種,歸納起來,組件關系可分為父子組件通信、兄弟組件通信、跨級組件通信。本節將介紹各種組件之間通信的方法。

7.3.1自定義事件

? ? ? ? 當子組件需要向父組件傳遞數據時,就要用到自定義事件。我們在介紹指令v-on時有提到,v-on除了監聽DOM事件外,還可以用于組件之間的自定義事件。

? ? ? ? 如果你了解JavaScript的設計模式--觀察者模式,一定知道dispatchEvent和addEventListener這兩個方法。Vue組件也有與之類似的一套模式,子組件用$emit()來觸發事件,父組件用$on()來監聽子組件的事件。

? ? ? ? 父組件也可以直接在子組件的自定義標簽上使用v-on來監聽子組件觸發的自定義事件,示例代碼如下:

<div id="app">

? ? ? ? <p>總數:{{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>

? ? ? ? 上面示例中,子組件有兩個按鈕,分別實現加一和減一的效果,在改變組件的data"counter"后,通過$.emit()再把它傳遞給父組件,父組件用v-on:increase和v-on:reduce,$emit()方法的第一個參數是自定義事件的名稱,例如示例的increase和reduce后面的參數都是要傳遞的數據,可以不填或填寫多個。

? ? ? ? 除了用v-on在組件上監聽自定義事件,也可以監聽DOM事件,這時可以用.native修飾符表示監聽的是一個原生事件 ,監聽的是該組件的根元素,示例代碼如下:

<my-component v-on:click.native="handleClick"></my-component>

7.3.2使用v-model

? ? ? ? Vue2.x可以在自定義組件上使用v-model指令,我們先看一個示例:

<div id="app">

? ? <p>總數:{{ 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>

? ? ? ? 仍然是點擊按鈕加一的效果,不過這次組件$emit()的事件名是特殊的input,在使用組件的父級,并沒有在<my-component>上使用@input="handler",而是直接用了v-model綁定一個數據total。這也可以稱作一個語法糖,因為上面的示例可以間接地用自定義事件來實現:

……

<my-component @input="handleGetTotal|"></my-component>

……

var app =new Vue({

? ? el:'#app',

? ? data:{totoal:0},

? ? methods:{

? ? ? ? handleGetTotal:function(total){

? ? ? ? this.total=total;

? ? ? ? }

? ? }

})

v-model還可以用來創建自定義的表單輸入組件,進行數據雙向綁定,例如:

<div id="app">

? ? <p>總數:{{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>

實現這樣一個具有雙向綁定的v-model組件要滿足下面兩個要求:

接收一個value屬性

在有新的value時觸發input事件。

7.3.3非父子組件通信

? ? ? ? 在實際業務中,除了父子組件通信外,還有很多非父子組件通信的場景,非父子組件一般有兩種,兄弟組件和跨多級組件,為了更加徹底地了解Vue.js2.x中的通信方法,我們先來看一下在Vue.js1.x中是如何實現的,這樣便于我們了解Vue.js的設計思想。

? ? ? ? 在Vue.js1.x中,除了$emit()方法外,還提供了$dispatch()和$broadcast()這兩個方法。$dispatch()用于向上級派發事件,只要是它的父級(一級或多級以上),都可以在Vue實例的events選項內接收,實例代碼如下:

<!--注意:該示例需使用Vue.js1.x的版本-->

<div id="app">

? ? {{ message }}

? ? <my-component></my-component>

</div>

<script>

? ? Vue.component(my-component',{

? ? ? ? template:'<buttonj @click="handleDispatch">派發事件</button>',

? ? ? ? methods:{

? ? ? ? ? ? handleDispatch:function(){

? ? ? ? ? ? ? ? this.$dispatch('on-message','來自內部組件的數據');

? ? ? ? ? ? }

? ? ? ? }

? ? }');

? ? var app = new Vue({

? ? ? ? el:'#app',

? ? ? ? data:{

? ? ? ? message:''

? ? ? ? },

? ? ? ? event:{

? ? ? ? "on-message":function(msg){

? ? ? ? ? ? this.message = msg ;

? ? ? ? }

? ? }

})

</script>

? ? ? ? 同理,$broadcast()是由上級向下級廣播事件的,用法完全一致,只是方向相反。

? ? ? ? 這兩種方法一旦發出事件后,任何組件都是可以接收到的,就近原則,而且會在第一次接收到后停止冒泡,除非返回true。

? ? ? ? 這兩個方法雖然看起來很好用,但是在Vue.js2.x中都廢棄了,因為基于組件樹結構的事件流方式讓人難以理解,并且在組件結構擴展的過程中會變得越來越脆弱,并且不能解決兄弟組件通信的問題。

? ? ? ? 在Vue.js2.x中,推薦使用一個空的Vue實例作為中央事件總線(bus),也就是一個中介。為了更形象的了解它,我們舉一個生活中的例子:

? ? ? ? 比如你需要租房子,你可能會找房產中介來登記你的需求,然后中介把你的信息發給滿足要求的出租者,出租者再把報價和看房的時間告訴中介,由中介再轉達給你,整個過程中,買家和賣家并沒有任何交流,都是通過中介來傳話的。

? ? ? ? 或者你最近可能要換房了,你會找房產中介登記你的信息,訂閱與你找房需求相關的資訊,一旦有符合你的房子出現時,中介會通知你,并傳達你房子的具體信息。

? ? ? ? 這兩個例子中,你和出租者擔任的就是兩個跨級的組件,而房產中介就是這個中央事件總線(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','來自組件component-a的內容');

? ? ? ? ? ? }

? ? ? ? }

? ? });

? ? var app = new Vue({

? ? ? ? el:'#app',

? ? ? ? data:{

? ? ? ? message:''

? ? ? ? },

? ? ? ? mounted:function(){

? ? ? ? var _this = this;

? ? //在實例初始化時,監聽來自bus實例的事件

? ? bus.$on('on-message',function(msg){

? ? ? ? _this.message = msg;

? ? ? ? ? });

? ? ? ? }

? ? })

</script>

? ? ? ? 首先創建了一個名為bus的空Vue實例,里面沒有任何內容;然后全局定義了組件component-a;最后創建Vue實例app,在app初始化時,也就是在生命周期mounted鉤子函數里監聽了來組bus的事件on-message,而在組件component-a中,點擊按鈕會通過bus把事件on-message發出去,此時app就會接受到來自bus的事件,進而在回調里完成自己的業務邏輯。

? ? ? ? 這種方法巧妙而輕量地實現了任何組件間的通信,包括父子、兄弟、跨級,而且Vue1.x和Vue2.x都適用。如果深入使用,可以擴展bus實例,給它添加data、methods、computed等選項,比如用戶登錄的昵稱、性別、郵箱等,還有用戶的授權token等。只需在初始化時讓bus獲取一次,任何時間、任何組件就可以從中直接使用了,在單頁面富應用(SPA)中會很實用,我們會在進階篇里逐步介紹這些內容。

? ? ? ? 當你的項目比較大,有很多的小伙伴參與開發時,也可以選擇更好的狀態管理解決方案vuex,在進階篇里會詳細介紹關于它的用法。

? ? ? ? 除了中央事件總線bus外,還有兩種方法可以實現組件間通信:父鏈和子組件索引。

父鏈

? ? ? ? 在子組件中,使用this.$parent可以直接訪問該組件的父實例或組件,父組件也可以通過this.$children訪問它所有的子組件,而且可以遞歸向上或向下無限訪問,直到根實例或最內層的組件。示例代碼如下:

<div id="app">

? ? {{ message }}

? ? <component-a></component-a>

</div>

<script>

? ? Vue.component('component-a',{

? ? ? ? template:'<button @click="handleEvent">通過父鏈直接修改數據</button>',

? ? ? ? methods:{

? ? ? ? handleEvent:function(){

? ? ? ? ? ? ? ? //訪問到父鏈后,可以做任何操作,比如直接修改數據

? ? ? ? ? ? ? ? this.$parent.message = '來自組件component-a的內容';

? ? ? ? ? ? }

? ? ? ? }

? ? });

? ? var app =new Vue({

? ? el:'#app',

? ? data:{

? ? ? ? message:''

? ? }

})

</script>

? ? ? ? 盡管Vue允許這樣操作,但在業務中,子組件應該盡可能地避免依賴父組件的數據,更不應該主動的去修改它的數據,因為這樣使得父子組件緊耦合,只看父組件,很難理解父組件的狀態,因為它可能被任意組件修改,理想情況下,只有組件自己能修改它的狀態。父子組件最好還是通過props和$emit來通信。

子組件索引

? ? ? ? 當子組件較多時,通過this.$children來一一遍歷出我們需要的一個組件實例是比較困難的,尤其是組件動態渲染時,它們的序列是不固定的。Vue提供了子組件索引的方法,用特殊的屬性ref來為子組件指定一個索引名稱,示例代碼如下:

<div id="app">

? ? <button @click="handleRef">通過ref獲取子組件實例</button>

? ? <component-a ref="comA"></component-a>

</div>

<script>

? ? ? ? Vue.component('component-a',{

? ? ? ? template:'<div>子組件</div>',

? ? ? ? data:function(){

? ? ? ? ? ? message:'子組件內容'

? ? },

? ? var app =new Vue({

? ? ? ? el:'#app',

? ? ? ? methods:{

? ? ? ? ? ? handleRef:fucntion(){

? ? ? ? ? ? ? ? var msg = this.$refs.comA.message;

? ? ? ? ? ? ? ? console.log(msg);

? ? ? ? ? ? }

? ? ? ? }

? ? })

});

</script>

? ? ? ? 在父組件模板中,子組件標簽上使用ref指定一個名稱,并在父組件內通過this.$refs來訪問指定名稱的子組件。

注意:$refs只在組件渲染完成后才填充,并且它是非響應式的。它僅僅作為一個直接訪問子組件的應急方案,應當避免在模板或計算屬性中使用$refs.

? ? ? ? 與Vue1.x不同的是,Vue2.x將v-el和v-ref合并為了ref,Vue會自動去判斷是普通標簽還是組件??梢試L試補全下面的代碼,分別打印出兩個ref看看都是什么:

<div id="app">

? ? <p ref="p">內容</p>

? ? <child-component ref="child"></child-component>

</div>


7.4使用slot分發內容

7.4.1什么是slot

? ? ? ? 看一個常規網站布局,一般由一級導航、二級導航、左側列表、正文以及底部版權信息5個模塊組成,如果要講它們都組件化,這個結構可能會是:

<app>

? ? <menu-main></menu-main>

? ? <menu-sub></menu-sub>

? ? <div class="container">

? ? ? ? <menu-left<</menu-left>

? ? ? ? <container></container>

? ? </div>

? ? <app-footer></app-footer>

</app>? ?

? ? ? ? 當需要讓組件組合使用,混合父組件的內容與子組件的模板時,就會用到slot,這個過程叫做內容分發(transclusion)。以<app>為例,它有兩個特點:

<app>組件不知道它的掛載點會有什么內容。掛載點的內容是由<app>的父組件決定的。

<app>組件很可能有它自己的模板。

? ? ? ? props傳遞數據,enents觸發事件和slot內容分發就構成了Vue組件的三個API來源,再復雜的組件也是由這三部分構成的。

7.4.2作用域

? ? ? ? 正式介紹slot前,需要先指定一個概念:編譯的作用域。比如父組件中有如下模板:

<child-component>

? ? {{ message }}

</chid-component>

? ? ? 這里的message就是一個slot,但是它綁定的是父組件的數據,而不是組件<chid-component>的數據。

? ? ? ? 父組件模板的內容是在父組件作用域內編譯,子組件模板的內容是在子組件作用域內編譯。例如下面的代碼 示例:

<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>

這里的狀態showChild綁定的是父組件的數據,如果想在子組件上綁定,那應該是:

<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分發的內容,作用域是在父組件上的。

7.4.3 slot用法

單個slot

在子組件內使用特殊的<slot>元素就可以為這個子組件開啟一個slot(插槽),在父組件模板里,插入子組件標簽內的所有內容將替代子組件的<slot>標簽以及它的內容。示例代碼如下:

<div id="app">

<child-component>

<p>分發的內容</p>

<p>更多分發的內容</p>

</child-component>

</div>

<script>

Vue.component('child-component',{

template:'\

<div>\

<slot>\

<p>如果父組件沒有插入內容,我將作為默認出現</p>\

</slot>\

</div>'

});

var app = new Vue({

el:'#app'

})

</script>

具名Slot

給<slot>元素指定一個name后可以分發多個內容,具名Slot可以與單個Slot共存,例如下面的示例:

<div id="app">

<child-component>

<h2 slot="header">標題</h2>

<p>正文內容</p>

<p>更多的正文內容</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>

子組件內聲明了3個<slot>元素,其中在<div class="main">內的<slot>沒有使用name屬性,它將作為默認slot出現,父組件沒有使用slot特性的元素與內容都將出現在這里。

如果沒有指定默認的匿名slot,父組件內多余的內容片段都將被拋棄。

上例最終渲染后的結果為:

<div id="app">

? ? <div class="container">

????????<div class="header">

? ? ? ? ? ? ? ? <h2>標題</h2>

? ? ? ? </div>

? ? ? ? <div class="main">

? ? ? ? ? ? <p>正文內容</p>

? ? ? ? ? ? <p>更多的正文內容</p>

? ? ? ? </div>

? ? ? ? <div class="footer">

? ? ? ? ? ? <div>底部信息</div>

????????</div>

? ? </div>

</div>

在組合使用747組件時,內容分發API至關重要。

7.4.4作用域插槽

????????作用域插槽是一種特殊的slot,使用一個可以復用的模板替換已渲染元素。概念比較難理解,我們先看一個簡單的示例來了解它的基本用法。示例代碼如下:

<div id="app">

? ? <child-component>

? ? ? ? <template scope="props">

? ? ? ? ? ? <p>來自父組件的內容</p>

? ? ? ? ? ? <p>{{ props.msg}}</p>

? ? ? ? </template>

? ? </child-component>

</div>

<script>

? ? Vue.component('child-component',{

? ? ? ? template:'\

? ? ? ? ? ? <div class="container">\

? ? ? ? ? ? ? ? <slot msg="來自子組件的內容"></slot>\

? ? ? ? ? ? </div>'

});

var app = new Vue({

? ? el:'#app'

})

</script>

? ? ? ? 觀察子組件的模板,在<slot>元素上由一個類似props傳遞數據給組件的寫法msg="xxx",將數據傳到了插槽。父組件中使用了<template>元素,而且擁有一個scope="props"的特性,這里的props只是一個臨時變量,就像v-for="item in items"里面的item一樣。template內可以通過臨時變量props訪問來自子組件插槽的數據msg。

將上面的示例渲染后的最終結果為:

<div id="app">

<div class="container">

? ? <p>來自父組件的內容</p>

? ? <p>來自子組件的內容</p>

</div>

</div>

? ? ? ? 作用域插槽更具代表性的用例是列表組件,允許組件自定義應該如何渲染列表每一項。示例代碼如下:

<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實戰》'},

????????????{name: '《Vue.js實戰之六個周》'},

????????????{name: '《Vue.js實戰之簡書六個周》'},

????????]

????}

})

</script>

????????子組件my-list接收一個來自父級的props數組books,并且將它在name為book的slot上使用v-for指令循環,同時暴露一個變量bookName。

????????上示例注意是為了介紹作用域插槽的用法。

7.4.5訪問slot

? ? ? ? 在Vue.js1.x中,想要獲取某個slot是比較麻煩的,需要用v-el間接獲取。而Vue.js2.x提供了用來訪問被slot分發的內容的方法$slots,請看下面的示例:

<div id="app">

? ? <child-component>

? ? ? ? <h2 slot="header">標題</h2>

? ? ? ? <p>正文內容</p>

? ? ? ? <p>更多的正文內容</p>

? ? ? ? <div slot="footer">底部內容</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>

? ? ? ? 通過$slot可以訪問某個具名slot,this.$slot.default包括了所有沒有被包含在具名slot中的節點。嘗試編寫代碼,查看兩個console打印的內容。

? ? ? ? $slot在業務中幾乎用不到,在用render函數(進階篇中將介紹)創建組件時會比較有用,但注意還是用于獨立組件開發中。


更多內容,請訪問的我的個人博客:[https://liugezhou.github.io/blog](https://liugezhou.github.io/blog).

您也可以關注我的個人公眾號:【Wakaka】



上一章:vue.js-表單與v-model(基礎篇)

下一章:vue.js-自定義指令(基礎篇)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容