Vue 組件詳解

參考書籍:《Vue.js 實戰》數組

組件與複用

爲何使用組件

Vue 的組件就是提升複用性,讓代碼可複用。app

組件用法

<div id="app">
    <my-component></my-component>
</div>

組件須要註冊後纔可使用,註冊有全局註冊和局部註冊兩種方式。函數

  1. 全局註冊後,任何 Vue 實例均可以使用。組件化

    // 要在父實例中使用這個組件,必需要在實例建立前註冊。
    Vue.component('my-component', {
        template: '<div>my component</div>'
    });
    
    var app = new Vue({
        el: '#app'
    });
  2. 在 Vue 實例中,使用 components 選項能夠局部註冊組件,註冊後的組件只有在該實例做用域下有效。佈局

    var Child = {
        template: '<div>my component</div>'
    };
    
    var app = new Vue({
        el: '#app',
        components: {
            'my-component': Child
        }
    });

渲染後的結果是:網站

<div id="app">
    <div>my component</div>
</div>

Vue 組件的模板在某些狀況下會收到 HTML 的限制,好比 <table> 內規定只容許是 <tr><td> 等這些表格元素,因此在 <table> 內直接使用組件是無效的,這種狀況下可使用特殊的
is 屬性來掛載組件。ui

<div id="app">
    <table>
        <tbody is="my-component"></tbody>
    </table>
</div>
Vue.component('my-component', {
    template: '<div>my component</div>'
});

var app = new Vue({
    el: '#app'
});

渲染後的結果是:this

<div id="app">
    <table>
        <div>my component</div>
    </table>
</div>

在組件中使用 data 時,必須是函數,而後將數據 return 出去。設計

Vue.component('my-component', {
    template: '<div>my component</div>',
    data() {
        return {
            message: 'message'
        }
    }
});

使用 props 傳遞數據

基本用法

組件不只僅是要把模板的內容進行復用,更重要的是組件間要進行通訊。一般父組件的模板中包含子組件,父組件要正向地向子組件傳遞數據或參數,子組件接收到後根據參數的不一樣來渲染不一樣的內容或執行操做。這個正向傳遞數據的過程就是經過 props 來實現的。雙向綁定

props 中聲明的數據與組件 data 函數內的數據主要區別就是 props 的數據來自父級,而 data 的數據是組件本身的數據,這兩種數據均可以在模板 template 、計算屬性 computed 和方法 methods 中使用。

一般,傳遞的數據並非直接寫死的,而是來自父級的動態數據,這時可使用指令 v-bind 來動態綁定 props 的值,當父組件的數據變化時,也會傳遞給子組件。

因爲 HTML 特性不區分大小寫,當時用 DOM 模板時,駝峯命名(camelCase)的 props 名稱要轉爲短橫分隔命名(kebab-case)。

<div id="app">
    <input type="text" v-model="parentMessage" />
    <my-component :my-message="parentMessage"></my-component>
</div>
Vue.component('my-component', {
    props: ['myMessage'],
    template: '<div>{{ myMessage}}</div>'
});

var app = new Vue({
    el: '#app',
    data: {
        parentMessage: ''
    }
});

渲染後的結果是:

<div id="app">
    <div>dataMes</div>
</div>

這裏用 v-model 綁定了父級的數據 parentMessage,當經過輸入框任意輸入時,子組件接收到的 props 也會實時響應,並更新組件模板。

單向數據流

Vue 經過 props 傳遞數據是單向的,也就是父組件數據變化時會傳遞給子組件,可是反過來不行。之因此這樣設計,是儘量將父子組件解耦,避免子組件無心中修改了父組件的狀態。

業務中會常常遇到兩種須要改變 prop 的狀況。

  1. 一種是父組件傳遞初始值進來,子組件將它做爲初始值保存起來,在本身的做用域下能夠隨意使用和修改。

    <div id="app">
        <my-component :init-count="1"></my-component>
    </div>
    Vue.component('my-component', {
        props: ['initCount'],
        template: '<div>{{ count }}</div>',
        data() {
            return {
                count: this.initCount
            }
        }
    });
    
    var app = new Vue({
        el: '#app'
    });

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

  2. 另外一種狀況就是 prop 做爲須要被轉變的原始值傳入。

    <div id="app">
        <my-component :width="100"></my-component>
    </div>
    Vue.component('my-component', {
        props: ['width'],
        template: '<div :style="style">組件內容</div>',
        computed: {
            style() {
                return {
                    width: this.width + 'px'
                }
            }
        }
    });
    
    var app = new Vue({
        el: '#app'
    });

    由於用 CSS 傳遞寬度要帶單位(px),可是每次都寫太麻煩了,並且數值計算通常是不帶單位的,因此統一在組件內使用計算屬性就能夠了。

數據驗證

Vue.component('my-component', {
    props: {
        propA: Number,
        propB: [String, Number],
        propC: {
            type: Boolean,
            default: true
        },
        propD: {
            type: Number,
            required: true
        },
        // 若是是數組或對象,默認值必須是一個函數來返回
        propE: {
            type: Array,
            default() {
                return [];
            }
        },
        propF: {
            validator(value) {
                return value > 10;
            }
        }
    }
});

組件通訊

組件關係可分爲父子組件通訊、兄弟組件通訊和跨級組件通訊。

自定義事件

當子組件須要向父組件傳遞數據時,就要用到自定義事件。

子組件用 $emit() 來觸發事件,父組件用 $on 來監聽子組件的事件。

父組件也能夠直接在子組件的自定義標籤上使用 v-on 來監聽子組件觸發的自定義事件。

<div id="app">
    <p>總數:{{ total }}</p>
    <my-component
        @increase="handleGetTotal"
        @reduce="handleGetTotal">
    </my-component>
</div>
Vue.component('my-component', {
    template: `
        <div>
            <button @click="handleIncrease">+1</button>
            <button @click="handleReduce">-1</button>
        </div>
    `,
    data() {
        return {
            counter: 0
        }
    },
    methods: {
        handleIncrease() {
            this.counter++;
            this.$emit('increase', this.counter);
        },
        handleReduce() {
            this.counter--;
            this.$emit('reduce', this.counter);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleGetTotal(total) {
            this.total = total;
        }
    }
});

上面示例中,在改變組件的 counter 後,經過 $emit() 再把它傳遞給父組件。$emit() 方法的第一個參數是自定義事件的名稱,後面的參數是要傳遞的數據,能夠不填或填寫多個。

除了用 v-on 在組件上監聽自定義事件外,也能夠監聽 DOM 事件,這時能夠用 .native 修飾符表示監聽的是一個原生事件,監聽的是該組件的根元素。

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

使用 v-model

Vue 能夠在自定義組件上使用 v-model 指令。

<div id="app">
    <p>總數:{{ total }}</p>
    <my-component v-model="total"></my-component>
</div>
Vue.component('my-component', {
    template: '<button @click="handleIncrease">+1</button>',
    data() {
        return {
            counter: 0
        }
    },
    methods: {
        handleClick() {
            this.counter++;
            this.$emit('input', this.counter);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    }
});

在使用組件的父級,並無在 <my-component> 使用 @input="handler",而是直接用了 v-model 綁定的一個數據 total。這也能夠稱做是一個語法糖,由於上面的示例能夠間接地用自定義事件來實現:

<div id="app">
    <p>總數:{{ total }}</p>
    <my-component @input="handleGetTotal"></my-component>
</div>
// 省略組件代碼

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleGetTotal() {
            this.total = total;
        }
    }
});

v-model 還能夠用來建立自定義的表單輸入組件,進行數據雙向綁定。

<div id="app">
    <p>總數:{{ total }}</p>
    <my-component v-model="total"></my-component>
    <button @click="handleReduce">-1</button>
</div>
Vue.component('my-component', {
    props: ['value'],
    template: '<input :value="value" @input="updateValue" />',
    methods: {
        updateValue(event) {
            this.$emit('input', event.target.value);
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        total: 0
    },
    methods: {
        handleReduce() {
            this.total--;
        }
    }
});

實現這樣一個具備雙向綁定的 v-model 組件要知足下面兩個條件:

  1. 接收一個 value 屬性。
  2. 在有新的 value 時觸發 input 事件。

非父子組件通訊

非父子組件通常有兩種,兄弟組件和跨多級組件。

在 Vue 中,推薦使用一個空的 Vue 實例做爲中央事件總線(bus),也就是一箇中介。

<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
var bus = new Vue();

Vue.component('component-a', {
    template: '<button @click="handleEvent">傳遞事件</button>',
    methods: {
        handleEvent() {
            bus.$emit('on-message', '來自組件 component-a 的內容');
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        message: ''
    },
    mounted() {
        var _this = this;
        
        // 在實例初始化時,監聽來自 bus 實例的事件
        bus.$on('on-message', function(msg) {
            _this.message = msg;
        });
    }
});

首先建立一個名爲 bus 的空 Vue 實例,而後定義全局組件 component-a,最後建立 Vue 實例 app。在 app 初始化時,監聽了來自 bus 的事件 on-message,而在組件 component-a 中,點擊按鈕會經過 bus 把事件 on-message 發出去,此時 app 就會接收到來自 bus 的事件,進而在回調裏完成本身的業務邏輯。

這種方法巧妙而輕量地實現了任何組件間的通訊,包括父子、兄弟和跨級。若是深刻使用,能夠擴展 bus 實例,給它添加 datamethodscomputed 等選項,這些都是能夠共用的,在業務中,尤爲是協同開發時很是有用,由於常常須要共享一些通用的信息,好比用戶登陸的暱稱、性別、郵箱和受權等。只需子安初始化時讓 bus 獲取一次,任什麼時候間、任何組件就能夠從中直接使用,在單頁面富應用(SPA)中會很實用。

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

父鏈

在子組件中,使用 this.$parent 能夠直接訪問該組件的父實例或組件,父組件也能夠經過 this.$children 訪問它全部的子組件,並且能夠遞歸向上或向下無限訪問,直到根實例或最內層的組件。

<div id="app">
    {{ message }}
    <component-a></component-a>
</div>
Vue.component('component-a', {
    template: '<button @click="handleEvent">經過父鏈直接修改數據</button>',
    methods: {
        handleEvent() {
            // 訪問到父鏈後,能夠作任何操做,好比直接修改數據
            this.$parent.message = '來自組件 component-a 的內容'
        }
    }
});

var app = new Vue({
    el: '#app',
    data: {
        message: ''
    }
});

儘管 Vue 容許這樣操做,但在業務中,子組件應該儘量避免依賴父組件的數據,更不該該去主動修改它的數據,由於這樣使得父子組件耦合,只看父組件,很難理解父組件的狀態,由於它可能被任意組件修改,理想狀況下,只有組件本身能修改它的狀態。父子組件最好仍是經過 props$emit() 來通訊

子組件索引

當子組件較多時,經過 this.$children 來一一遍歷出咱們須要的一個組件實例時比較困難的,尤爲是組件動態渲染時,它們的序列是不固定的。Vue 提供了子組件索引的方法,用特殊的屬性 ref 來爲子組件指定一個索引名稱。

<div id="app">
    <button @click="handleRef">經過 ref 獲取子組件實例</button>
    <component-a ref="comA"></component-a>
</div>
Vue.component('component-a', {
    template: '<div>子組件</div>',
    data() {
        return {
            message: '子組件內容'
        }
    }
});

var app = new Vue({
    el: '#app',
    methods: {
        handleRef() {
            // 經過 $refs 來訪問指定的實例
            var msg = this.$refs.comA.message;
            console.log(msg);
        }
    }
});

提示:$refs 只在組件渲染完成後才填充,而且它是非響應式的。它僅僅做爲一個直接訪問子組件的應急方案,應當避免在模板或計算屬性中使用 $refs

使用 slot 分發內容

什麼是 slot

下面是一個常規的網站佈局組件化後的機構:

<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> 爲例,它有兩個特色:

  1. <app> 組件不知道它的掛載點會有什麼內容。掛載點的內容由 <app> 的父組件決定。
  2. <app> 組件極可能有它本身的模板。

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

做用域

父組件模板的內容是在父組件做用域內編譯,子組件模板的內容是在子組件做用域內編譯。

<div id="app">
    <child-component v-show="showChild"></child-component>
</div>
Vue.component('child-component', {
    template: '<div>子組件</div>'
});

var app = new Vue({
    el: '#app',
    data: {
        showChild: true
    }
});

這裏的狀態 showChild 綁定的是父組件的數據,若是想在子組件上綁定,應該是:

<div id="app">
    <child-component></child-component>
</div>
Vue.component('child-component', {
    template: '<div v-show="showChild">子組件</div>',
    data() {
        return {
            showChild: true
        }
    }
});

var app = new Vue({
    el: '#app'
});

所以,slot 分發的內容,做用域是在父組件上的。

slot 用法

單個 slot

在子組件內使用特殊的 <slot> 元素就能夠爲這個子組件開啓一個 slot(插槽),在父組件模板裏,插入在子組件標籤內的全部內容將替代子組件的 <slot> 標籤及它的內容。

<div id="app">
    <child-component>
        <p>分發的內容</p>
        <p>更多分發的內容</p>
    </child-component>
</div>
Vue.component('child-component', {
    template: `
        <div>
            <slot>
                <p>若是父組件沒有插入內容,我將做爲默認出現</p>
            </slot>
        </div>
    `
});

var app = new Vue({
    el: '#app'
});

上例渲染後的結果是:

<div id="app">
    <div>
        <p>分發的內容</p>
        <p>更多分發的內容</p>
    </div>
</div>

注意:子組件 <slot> 內的備用內容,它的做用域是子組件自己。

具名 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>
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>
    `
});

var app = new Vue({
    el: '#app'
});

上例渲染後的結果是:

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

注意:若是沒有指定默認的匿名 slot,父組件內多餘的內容片斷都將被拋棄。

做用域插槽

做用域插槽是一種特殊的 slot,使用一個能夠複用的模板替換已渲染元素。

<div id="app">
    <child-component>
        <template scope="props">
            <p>來自父組件的內容</p>
            <p>{{ props.msg }}</p>
        </template>
    </child-component>
</div>
Vue.component('child-component', {
    template: `
        <div class="container">
            <slot msg="來自子組件的內容"><slot>
        </div>
    `
});

var app = new Vue({
    el: '#app'
});

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

上例渲染後的結果是:

<div id="app">
    <child-component>
        <p>來自父組件的內容</p>
        <p>來自子組件的內容</p>
    </child-component>
</div>

做用域插槽更具表明性的用例是列表組件,容許組件自定義應該如何渲染列表每一項。

<div id="app">
    <my-list :books="books">
        <!-- 做用域插槽也能夠是具名的 slot -->
        <template slot="book" scope="props">
            <li>{{ props.bookName }}</li>
        </template>
    </my-list>
</div>
Vue.component('my-list', {
    props: {
        books: {
            type: Array,
            default() {
                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: '《book1》' },
            { name: '《book2》' },
            { name: '《book3》' }
        ]
    }
});

子組件 my-list 接收一個來自父級的 prop 數組 books,而且將它在 namebookslot 上使用 v-for 指令循環,同時暴露一個變量 bookName

此例的用意主要是介紹做用域插槽的用法,並無加入使用場景,而做用域插槽的使用場景既能夠複用子組件的 slot,又能夠是 slot 內容不一致。若是此例還在其餘組件內使用,<li> 的內容渲染權是由使用者掌握的,而數據卻能夠經過臨時變量(好比 props)從子組件內獲取。

訪問 slot

Vue 提供了用來訪問被 slot 分發的內容的方法 $slots

<div id="app">
    <child-component>
        <h2 slot="header">標題</h2>
        <p>正文內容</p>
        <p>更多的正文內容</p>
        <div slot="footer">底部信息</div>
    </child-component>
</div>
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>
    `,
    mounted() {
        var header = this.$slots.header,
            main = this.$slots.default,
            footer = this.$slots.footer;
            
        console.log(footer);
        console.log(footer[0].elm.innerHTML);
    }
});

var app = new Vue({
    el: '#app'
});

經過 $slots 能夠訪問某個具名 slotthis.$slots.default 包括了全部沒有被包含在具名 slot 中的節點。

$slots 在業務中幾乎用不到,在用 render 函數建立組件時會比較有用,但主要仍是用於獨立組件開發中。

相關文章
相關標籤/搜索