理解vue中的組件(二)

上節說到組件http://www.javashuo.com/article/p-thaozczc-dc.html,這一節繼續來學習組件:javascript

原文博客地址,歡迎學習交流: 點擊預覽
從github上獲取本文代碼: 示例代碼

封裝的組件要具有複用性和通用性。css

先來講複用,複用主要是複用 HTML 結構,外加這塊結構中的交互 js,和針對這一塊設置的 css。 這三者是構成一個組件最基本的要素,這三者相互隔離有相互做用,將三者聚合起來,在須要使用的地方,相似一個變量(標籤對)同樣,會引用這一塊的全部功能,能夠屢次使用。html

在 vue 中提供了單文件組件,一個文件就是一個組件,這樣把組件模塊化的方式,讓開發者更方便的利用組件堆積頁面。將三者聚合在一個文件中,孤立的存在,減小了改動組件而影響外界的風險,極大的提升了代碼可維護性。vue

再說通用性,在討論通用性這點上,要向兩個方面思考:java

  1. 外界使用組件,對組件所須要的數據進行定製,由外界傳遞進來(內部能夠設置默認值)
  2. 組件內部的交互要通知給外界,並在外界的控制下產生影響,作不一樣事情。

組件達到複用後,能夠在多個地方使用,而使用的位置不一樣,須要展現的數據也不一樣,此時封裝的組件要具備通用性,組件內部則由外界使用組件時來決定將要顯示的數據,須要將數據傳遞給組件。git

組件的內部除了須要數據外,不可避免的還有交互,當完成一個交互後,須要對外界產生影響,這不能在組件內部作具體的事情,由於使用的位置不一樣,所產生的效果也不同,而完成這一系列事情則交給外界來決定,須要組件內部通訊給外界,告訴外界,內部完成了一次交互。github

註冊使用組件

從封裝一個自定義的下拉框 custom-select 組件開始。segmentfault

要達到封裝性好,而且能夠寫多種功能的代碼塊,那麼組件自己就是一個函數或者類,須要使用 Vue.extend( options ) 來建立構造器,這個構造器能夠由開發者本身手動初始化掛載,也能夠註冊成組件在其餘組件的模板中使用。數組

在 body 中放置掛載點:瀏覽器

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

定義組件的構造器,並手動初始化,手動掛載:

let customeSelect = Vue.extend({
    template: `
        <div class="select">
            <h2>這是一個定義的下拉框</h2>
            <p>請選擇:北京</p>
            <ul>
                <li>北京</li>
                <li>上海</li>
                <li>杭州</li>
            </ul>
        </div>
    `
})
// 手動初始化,掛載到頁面的掛載點上
new customeSelect().$mount('#app');

選擇手動初始化的方式,調用內置方法 $mount 方法進行掛載,隨後組件的模板進行編譯,替換掉掛載點,渲染在頁面中。

每每定義組件的構造器後,不須要手動的進行初始化,而是在其餘組件的模板中當成標籤來使用,這時候須要調用 Vue.component( id, [definition] ) 註冊成組件。

// 註冊組件,傳入一個擴展過的構造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 註冊組件,傳入一個選項對象 (自動調用 Vue.extend)
Vue.component('my-component', { /* ... */ })

// 獲取註冊的組件 (始終返回構造器)
var MyComponent = Vue.component('my-component')

根據註冊組件的語法,實際上是能夠省略調用 Vue.extend 這一步,只須要傳入 選項對象便可,內部會自定調用 Vue.extend ,因此定義組件變成了這樣的簡寫方式:

Vue.component('custome-select',{
    template: `
        <div class="select">
            <h2>這是一個定義的下拉框</h2>
            <p>請選擇:北京</p>
            <ul>
                <li>北京</li>
                <li>上海</li>
                <li>杭州</li>
            </ul>
        </div>
    `
})

未來 custome-select 就當成了標籤使用在其餘組件的模板中 < custome-select>< /custome-select>,Vue在編譯模板時,就回去找這種自定義標籤是不是一個組件,若是已經註冊的話,就會把註冊的構造器進行初始化,編譯組件模板,最終將編譯後的模板替換掉自定義標籤的位置。若是沒有註冊直接使用,則會拋出錯誤。

關於組件名稱的命名:

  • 採用烤串(kebab-case)命名,custome-select
  • 採用駝峯命名( PascalCase), customeSelect
  • 名稱不能是HTML規定的標籤名,好比div、span、header、footer等等。。。
注意:註冊時隨便使用兩種命名方式的任何一種,在模板中一概採用烤串命名纔有效。

定義掛載點,並使用組件:

<div id="app">
    <custome-select></custome-select>
</div>

啓動應用:

new Vue({
    el: '#app'
})

最終渲染後的結構爲:

<div id="app">
    <!--如下替換了原來 <custome-select></custome-select> 標籤位置-->
    <div class="select">
        <h2>這是一個定義的下拉框</h2>
        <p>請選擇:北京</p>
        <ul>
            <li>北京</li>
            <li>上海</li>
            <li>杭州</li>
        </ul>
    </div>
</div>

給組件定製數據傳遞props

目前 HTML 達到了複用的目的,但使用屢次依然顯示的是寫死的數據。做爲顯示數據的 HTML 結構,在不一樣地方使用,所要展現的數據由外界來決定,這就須要給組件傳遞數據。

<div id="app">
    <!-- 城市的下拉 -->
    <custome-select></custome-select>
    <!-- 用戶的下拉 -->
    <custome-select></custome-select>
    <!-- 日期的下拉 -->
    <custome-select></custome-select>
</div>

而傳遞參數實際上就是給組件的構造器傳遞參數,本質上就是給函數傳參。函數的參數分爲實參和形參兩個部分:

  • 實參是實際傳遞給函數的參數
  • 形參是用來接收數據所聲明的變量

如今組件寫在模板中以標籤對的形式呈現,須要傳遞實際的參數,惟一的地方就是寫在行間做爲自定義屬性,而傳遞的參數會有不少個,最好代表具體的含義,須要和組件約定好屬性名,傳遞參數:

<div id="app">
    <!-- 城市的下拉 -->
    <custome-select title="請選擇城市" :list="['北京','上海','杭州']"></custome-select>
    <!-- 用戶的下拉 -->
    <custome-select title="請選擇用戶" :list="['張三','李四','王五']"></custome-select>
</div>

在組件中須要顯示的用 props 接收傳遞的數據,這樣的好處就是一旦看到組件,就會很清晰快速的瞭解到組件所須要的數據。

注意: 在行間寫上自定義屬性,要解析爲數組,在屬性名前加上 v-bind,解析爲 javascript 表達式,不然只能當成是字符串。

具體以下:

Vue.component('custome-select',{
    // 關於props具體參考:
    // https://cn.vuejs.org/v2/guide/components-props.html#Prop-%E7%B1%BB%E5%9E%8B
    // https://cn.vuejs.org/v2/guide/components-props.html#Prop-%E9%AA%8C%E8%AF%81
    props:{
        title: {
            type: String,
            default: '這是一個定義的下拉框'
        },
        list:{
            type: Array,
            default(){return []}
        },
        selectIndex:{
            type: Number,
            default:0
        }
    },
    template: `
        <div class="select">
            <h2>{{title}}</h2>
            <p>請選擇:{{list[selectIndex]}}</p>
            <ul>
                <li 
                    v-for="item,index in list"
                    :key="index"
                >
                    {{item}}
                </li>
            </ul>
        </div>
    `
})

在組件中約定了三個須要接收的參數,分別寫出了接受的類型和默認值,props參數文檔以下:

屬性 說明 類型 默認值
title 定製組件的標題 String '這是一個定義的下拉框''
list 定製組件的下拉列表 Array []
selectIndex 選擇要展現的一項 String 0

有了文檔,很清晰的知道每個屬性表明的意思,傳入響應的參數後,就會達到預期的效果。

組件自身狀態data

以上渲染後直接把下拉框顯示了出來,下拉框應該是在點擊 p 標籤時候才能顯示,再次點擊就隱藏掉,要實現這樣的一個顯示隱藏切換功能。

Vue 中不提倡直接操做 DOM,須要設置一個狀態來肯定 DOM 的狀態,當須要改變 DOM 時,只須要改變設置好的狀態便可,把咱們的關注點放在狀態的維護上,而無需手動操做 DOM 改變。

這個狀態不受外界的影響,屬因而組件自身的狀態變化,定義在組件內部,而且改變時只能由組件自身更改。

具體以下:

Vue.component('custome-select',{
    ... 省略了props設置
    data(){
        return {
            show: false  // 一開始狀態爲false,也就是不顯示下拉列表
        }
    },
    template: `
        <div class="select">
            <h2>{{title}}</h2>
            <p @click="toggleShow">請選擇:{{list[selectIndex]}}</p>
            <ul v-show="show">
                <li 
                    v-for="item,index in list"
                    :key="index"
                >
                    {{item}}
                </li>
            </ul>
        </div>
    `,
    methods:{
        toggleShow(){
            this.show = !this.show;
        }
    }
})

以上作了三件事情:

  1. data 中設置一個狀態爲 show,初始值爲 false,來表示下拉列表爲隱藏狀態
  2. 在模板上使用指令 v-show="show" 控制 DOM 的顯示隱藏
  3. p 綁定事件,切換 show 的值,一旦改變,自動更新 DOM 到對應狀態上,也就是 true 顯示,false 隱藏

Vuejs 這個框架要作的就是狀態和UI保持同步。

單向數據流

單向數據流顧名思義就是單方向的數據流向,也就是數據只能從一邊流向另外一邊,反過來則不行,如黃河之水從天上來,卻不能再流回到天上去。具體到組件中,就是:父子 prop 之間造成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中,子組件改變不能改變父組件。這樣設計的目的是防止從子組件意外改變父級組件的狀態,從而致使應用的數據流向難以理解。

與之對應的就是雙向數據流,父組件子組件均可以任意修改,互相產生影響,這樣的話使用這套數據的其餘組件也會跟着變化,變得很是的詭異。

在複雜的應用中,控制數據有規則的改變和傳遞很是重要,若是不是單向數據流的限制,任何組件都能修改數據,就跟定義全局的數據在任何程序都能修改同樣,最終通過多個函數的調用修改後,出現了問題,不能準確的定位到具體的函數中,排查問題會變的很是的困難。

每次父級組件發生更新時,子組件中全部的 prop 都將會刷新爲最新的值。由父組件傳遞給子組件的數據,子組件內部不能改變 prop。若是你這樣作了,Vue 會在瀏覽器的控制檯中發出警告。

來個例子說明一下。上面的例子中,須要在下拉框中選擇具體的的一項,顯示在 p標籤中,要顯示的數據是經過外界傳遞的 selectIndex 來決定從 list 中選取哪一項。那咱們能夠這樣來作,在點擊下拉框的某一項時,改變 selectIndex 爲點擊的一項的下標便可,具體以下:

HTML 代碼:

<div id="app">
    <!-- 城市的下拉 -->
    <custome-select 
        title="請選擇城市" 
        :list="['北京','上海','杭州']" 
        :select-index="0"
    ></custome-select>
</div>

JavaScript代碼:

Vue.component('custome-select',{
    props:{
        // 省略了title和list....
        selectIndex:{
            type: Number,
            default:0
        }
    },
    data(){
        return {
            show: false  // 一開始狀態爲false,也就是不顯示下拉列表
        }
    },
    template: `
        <div class="select">
            <h2>{{title}}</h2>
            <p @click="toggleShow">請選擇:{{list[selectIndex]}}</p>
            <ul v-show="show">
                <li 
                    v-for="item,index in list"
                    :key="index"
                    @click="changeIndex(index)"
                >
                    {{item}}
                </li>
            </ul>
        </div>
    `,
    methods:{
        toggleShow(){
            this.show = !this.show;
        },
        changeIndex(index){
            // 改變爲選中的下標,此時會報錯
            this.selectIndex = index;
        }
    }
})

new Vue({
    el: '#app'
})

以上代碼作的事情:

  1. 接收外界傳入的 selecteIndex,在模板中選擇對應的值{{list[selectIndex]}}
  2. 給下拉框的每一項綁定事件,並傳遞各自的下標
  3. 傳遞下標給到 changeIndex 函數,改變selectIndex的值
  4. 控制檯報錯:[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "selectIndex"大體的意思是:不能直接修改組件的props值,當父組件從新渲染時候會重寫這個。在組件中使用data或者computed屬性來代替修改prop的值。

以上的報錯已經警告了,不能直接修改props的值,可是組件內部是能夠修改組件內部數據 data ,因此修改以下:

// 其餘代碼省略
Vue.component('custome-select',{
    data(){
        return {
            currentIndex: this.selectIndex // 把selectIndex做爲currentIndex的初始值
        }
    },
    template: `
        <div class="select">
            <h2>{{title}}</h2>
            <p @click="toggleShow">請選擇:{{list[currentIndex]}}</p>
            <ul v-show="show">
                <li 
                    v-for="item,index in list"
                    :key="index"
                    @click="changeIndex(index)"
                >
                    {{item}}
                </li>
            </ul>
        </div>
    `,
    methods:{
        changeIndex(index){
            // 改變本身內部狀態currentIndex
            this.currentIndex = index;
        }
    }
})

以上代碼分析:

  1. 在 data 中定義組件內部狀態currentIndex,將props中的selectIndex的值,做爲currentIndex的初始值
  2. 修改模板中取值的selectIndex爲currentIndex
  3. 點擊改變currentIndex,此時修改的是組件內部狀態,不是props的值,修改爲功

從以上的例子中能夠看出來,data 中定義的就是組件內部狀態,只在組件內部更改,而傳遞的 props 不能在組件內修改,能夠經過賦值給data,修改data的值來更新組件自身的狀態。

父組件監聽,子組件發佈

上面說的是父組件向子組件定製數據傳遞 props,在子組件內部會產生一些交互。

子組件內部交互一旦發生後,父組件是須要根據子組件的交互會產生一些影響,好比改變顏色,顯示文字等。父組件這些變換又不能寫在子組件的交互中,由於子組件是通用的組件。一旦寫了某個父組件的業務代碼,只能和這個父組件綁定在一塊兒,不能使用在別的地方了了,此時組件不能達到通用的目的。

舉個例子:
如下使用了兩次 custome-select 組件,當點擊第一個組件的下拉框某一項時候,就須要改變 class 爲 test1 的div 樣式爲 red 色。當點擊第二個組件的下拉框某一項時候,就須要改變 class 爲 test2 的div 樣式爲 blue 色。

<div id="app">
    <!-- 城市的下拉 -->
    <div class="test1" :style="{color: color1}">第一個需求</div>
    <custome-select 
        title="請選擇城市" 
        :list="['北京','上海','杭州']" 
        :select-index="0">
    </custome-select>
    <!-- 用戶的下拉 -->
    <div class="test2" :style="{color: color2}">第二個需求</div>
    <custome-select 
        title="請選擇用戶" 
        :list="['張三','李四','王五']" 
        :select-index="1">
    </custome-select>
</div>

使用了兩次組件,組件內部點擊下拉框時不能寫具體的處理第一個需求仍是第二個需求。而是交到外部的父組件來決定,這時候父組件就須要知道子組件內部是否點擊了下拉框。而點擊下拉框這個動做是由用戶觸發的,不知什麼時候會觸發一次,那怎麼辦呢?
跟原生的元素處理思路同樣,假定之後用戶點擊了這個元素後,須要改變頁面中樣式,那麼就須要監控這個元素的點擊事件,只要用戶點擊了,觸發事件處理函數,在函數中寫具體改變樣式這個動做。
HTML代碼:

<button onclick="handle()">按鈕</button>

JavaScript代碼:

<script>
    // 全局設置函數
    function handle(){
        document.body.style.background = 'red'    
    }
</script>

以上代碼是 DOM0 級時代的寫法,直接在行間寫監聽事件,這樣寫更直觀。目的就是當有用戶點擊了按鈕一下,瀏覽器內部就會發佈一個 click 事件,而正好咱們在元素上監聽了 click 事件,就會把對應的事件處理函數觸發,從而達到開發者的目的,對頁面作出一些變化。

組件標籤使用在模板中,此時外界須要知道組件內部發生了的交互,那麼思路一致,也須要在行間監聽事件,不過此事件名字不限因而 w3c 規定的事件名,能夠自定義事件名,結合 Vue 中綁定事件的方式,代碼以下:

<div id="app">
    <!-- 城市的下拉 -->
    <div class="test1" :style="{color: color1}">第一個需求</div>
    <custome-select 
        title="請選擇城市" 
        :list="['北京','上海','杭州']" 
        :select-index="0"
        @click-option="changeTest1Handle"
    >
    </custome-select>
    <!-- 用戶的下拉 -->
    <div class="test2" :style="{color: color2}">第二個需求</div>
    <custome-select 
        title="請選擇用戶" 
        :list="['張三','李四','王五']" 
        :select-index="1"
        @click-option="changeTest2Handle"
    >
    </custome-select>
</div>

把事件處理函數寫在選項對象中:

new Vue({
    el: '#app',
    data: {
        color1: '',
        color2: ''
    },
    methods: {
        // 第一個需求
        changeTest1Handle(){
            this.color1 = 'red';
        },
        // 第二個需求
        changeTest2Handle(){
            this.color2 = 'blue';
        }
    }
})

以上代碼準備完畢,去點擊下拉選項,並無觸發父組件的函數,並無完成需求,爲何呢?
在原生元素上在行間監控事件,用戶點擊元素後,瀏覽器會發布 click 事件。而如今換作是使用自定義事件來監控子組件內部產生的交互,這就須要在子組件內部本身發佈這個自定義的事件,不然監控的自定義事件是無效的。

那何時發佈事件呢?就是在用戶點擊了下拉框的選項時候發佈這個自定義事件便可。

你能夠這樣來理解,監聽原生事件 click ,只須要監聽,開發者無需手動的在瀏覽器內部寫發佈事件,click 事件名是瀏覽器給開發者約定的名字。而如今咱們須要本身設計子組件發佈事件,父組件監聽這樣的機制。因此須要開發者本身約定事件的名字和手動的在組件中發佈事件。在 Vue 中這樣的訂閱/發佈模式已經寫好,開發者只須要調用便可。

在子組件中發佈事件:

// 其餘代碼省略
methods:{
    changeIndex(index){
        this.currentIndex = index;
        // 在點擊選項時候產生交互,手動發佈事件,通知父組件
        this.$emit('click-option');
    }
}

當點擊選項時候,父組件中會完成不一樣的需求,改變不一樣元素的顏色。

以上代碼父子組件之間徹底的解耦,父組件中不使用這個組件,依然能夠工做,子組件不使用在這個組件中,可使用在任意其餘的組件中。若是父組件關係子組件內部選中下拉框一項這個交互,只須要監聽 click-option這個自定義事件,不關心則不監聽。

總結

以上能夠看出一個組件數據的來源有兩個:

  • 組件自身的數據,寫在 data 中
  • 父組件傳遞的數據,寫在props中

父子組件之間通訊:

  • 父 ---> 子,使用 props
  • 子 ---> 父,訂閱發佈模式

以上屬於我的理解,若有誤差歡迎指正學習,謝謝。

相關文章
相關標籤/搜索