? ? ? 組件(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】