vue總結系列--組件化

上一篇博文梳理了vue的數據驅動和響應式相關的特性,這一篇博文就來梳理vue的一個很重要的特性,組件化。
自定義組件之於vue,其意義不亞於函數之於C,java之類的編程語言。
函數是計算機科學中的一大重要的發明。
一方面,它表明着一種自頂向下,逐步求精的分而治之的思惟,另一方面,它可以封裝複雜實現的細節,提供更高抽象的接口,下降軟件工程的複雜度。html

在vue中,自定義組件也起着相似的做用。vue

<!--more-->
咱們知道,在組件化的GUI界面上,GUI能夠被視爲一棵樹,瀏覽器的DOM就是一個最好的例子。
從佈局上來看,界面能夠當作大盒子套小盒子,小盒子再套更小的盒子。。。
反映到DOM上,DOM某節點的全部子節點,都是該組件的子組件,都是該組件內的元素。java

在vue中也是如此,vue組件之間的關係也是相似DOM同樣,是樹狀的。
在定義一個組件時,須要引用的全部組件,都成爲了該組件的子組件。git

組件通訊

組件做爲一個模塊性質的東西,天然就有着它必定的獨立性。並且,與其它模塊的耦合都理所應當的有着明確的接口約定。
在vue中,父子組件通訊經過組件屬性和事件來進行的。
其中,經過組件屬性,父組件的數據流向子組件;經過事件,子組件的數據流向父組件。github

從抽象的角度看,組件做爲一個黑盒子,它有着特定的屬性用以接收外部傳遞給它的數據,它也有着特定的事件,當特定操做發生時調用回調函數,以通知別的組件。編程

組件的屬性

自定義屬性

<div id="x-child">
    <span> {{ titleMessage }} <span>
</div>
<script>
    Vue.component('x-child', {
        template: '#x-child',
        props: ['titleMessage']
    });
</script>
<div id="x-parent">
    <x-child title-message="A"></x-child>
    <x-child title-message="B"></x-child>
    <x-child :title-message="message"></x-child>
</div>
<script>
    Vue.component('x-parent', {
        template: '#x-parent',
        data: function () {
            return {
                message: "C"
            };
        }
    });
</script>

上面的例子中,定義了x-child自定義組件,而且在x-parent組件中引用它。
以前在介紹數據驅動的時候,咱們知道,定義vue組件時,能夠經過data定義組件內部的狀態,它是組件數據的一部分。
除了data以外,prop(屬性)也是組件數據的來源之一,父組件經過prop將本身的數據傳遞給子組件。segmentfault

在定義組件時咱們能夠看到:數組

  1. 經過props定義組件可以接收的屬性,甚至還能指定屬性的默認值及類型,甚至還能編寫任意的函數驗證屬性的合法性。明確的指定相似接口聲明,加強可讀性,下降debug難度。
  2. 屬性和內部狀態相似,都做爲組件數據的一部分。區別在於,在vue設計上,屬性是隻讀的,能夠做爲數據驅動視圖,可是沒法被改變。(我不太清楚vue有沒有從語法強制要求這點,可是良好實踐的vue組件是這樣的)

在使用自定義組件時能夠看到:瀏覽器

  1. 組件屬性的靈活性特別強,你不只能傳遞給它一個固定的數據,還可以使用vue的數據綁定語法把父組件內的數據經過prop傳遞給子組件。
    固然,這也是響應式的。當父組件中該數據變化時,天然的,傳遞給子組件的數據也會變化。那麼,子組件中綁定了該數據的視圖部分也固然會被從新渲染以展示在瀏覽器上。
  2. 子組件中定義中的屬性是駝峯寫法,這是符合js編碼規範的。然而,在引用子組件的地方, 屬性應該要寫成短橫線分割式的寫法
    這是由於,html是不區分大小寫的,vue對此也很無奈。這是在實際編碼中很任意犯的一個錯誤,須要注意。

子組件的獨立性

vue中,屬性被設計用於父組件傳遞數據給子組件的,若是子組件改變了屬性,那麼父組件不會受到任何影響。這在vue中被稱爲 單向數據流
可是,若是屬性用來傳遞數組或對象等複合的數據結構,那麼可能會出問題。考慮如下的場景:數據結構

  1. 父組件把數據中的對象傳遞給了子組件的屬性。
  2. 因爲可能修改該屬性,子組件把該屬性直接賦值給內部狀態,做爲內部狀態修改。
  3. 在某些操做觸發後,該內部狀態被修改。

問題在於,因爲js是引用類型語言,簡單的賦值僅僅是傳遞引用,那麼,以上場景中,父組件中的數據,子組件中的屬性,還有子組件中的狀態, 指向的都是同一份對象
這會形成一個問題,若是子組件修改了該對象的屬性,那麼父組件的數據也會受到影響,這破壞了單向數據流,會形成不少詭異的bug。

解決方法也很顯然:

  1. 方案一是父組件傳遞數組或對象給子組件時,使用深拷貝拷貝一份過去,或者子組件將屬性賦值給內部狀態時,深拷貝一份過去,這樣就可以互不干擾。
  2. 方案二是使用不可變數據結構,每次修改都是產生新的拷貝,所以也能解決問題。

組件的事件

自定義事件

<div id="x-child">
  <button @click="onClick">click</button>
</div>

<script>
Vue.component('x-child', {
  template: '#x-child',
  data: function() {
      return {
          counter: 0
      };
  },
  methods: {
      onClick: function() {
          this.counter++;
          this.$emit("on-counter-add", this.counter);
      }
  }
});
</script>

以上自定義了x-child組件,而且自定義了組件事件。咱們能夠看到:

  1. 使用vue組件實例的$emit方法,用以觸發一個自定義事件。觸發事件能夠攜帶數據,這些數據被用於傳遞給綁定了事件的其它組件的回調函數上,進而被傳遞給其它組件。
  2. 不像屬性,自定義事件沒有一個統一聲明的地方,至於爲何我也不清楚。。。得問vue做者去。
  3. 該自定義組件內部包含了一個按鈕,按鈕被點擊事件觸發了自定義組件的回調函數,進而觸發了該自定義組件的自定義事件。
    從一個角度來看,該自定義組件像是轉發了原生組件的事件而已。可是從另一個角度來看,該自定義組件封裝了這些細節,對外展示的是一個點下按鈕觸發計數器增長的事件的這樣一個計數器。
  4. 事件名稱的定義用的短橫線分割的寫法,緣由和屬性相似。
<div id="x-parent">
    <x-child @on-counter-add="onCounterAdd"></x-child>
    <span> { { counter }} </span>
</div>
<script>
    Vue.component('x-parent', {
        template: '#x-parent',
        data: function () {
            return {
                counter: 0
            };
        },
        methods: {
            onCounterAdd: function (counter) {
                this.counter = counter;
            }
        }
    });
</script>

以上定義了x-parent組件,而且引用了上面定義的子組件。能夠看出:

  1. 子組件事件觸發了父組件的回調函數,而且將數據從回調函數中傳入。父組件能夠在回調函數裏作任何事情,很有靈活性。
  2. 通常狀況下,父組件會在回調函數中更新本身的狀態數據。數據更新後觸發新的視圖渲染,用戶便可在界面上看到了反饋。這樣,經過事件,子組件的數據傳遞到了父組件中。

事件綁定的表達式寫法

在監聽事件的地方,上面的寫法是使用了一個回調函數,不過,也可使用js表達式,好比:

<x-child @on-counter-add="counter = arguments[0]"></x-child>

上面代碼的重點在於arguments[0],若是是js表達式寫法,使用arguments引用事件的參數,就好像這段js表達式被放入了一個vue提供的匿名函數,而後使用匿名函數監聽這個事件同樣。
那它有什麼用呢?在上面的場景裏這樣寫固然是很差的,由於削弱了可讀性。

以前在我同事碰到的一個場景裏,是一個涉及到插槽分發做用域的場景,若是寫成回調函數的形式,那麼在回調函數中沒法訪問插槽做用域的變量。
所以,必須使用js表達式的寫法,將插槽做用域中的變量顯式的帶到回調函數中,代碼相似這種,懶得構造具體的例子了

<x-child @on-counter-add="onCountAdd(arguments[0], scope.id)"></x-child>

雙向綁定

因爲vue設計的父子組件通訊是單向數據流,可是因爲一些需求的須要,若是能提供雙向數據流,會使使用起來更方便。
便捷性和設計的統一性衝突,怎麼辦?固然是用語法糖解決了。

實際上,vue提供的兩種好像是雙向數據流的機制,.syncv-model ,都是語法糖。

.sync修飾符

<comp :foo.sync="bar"></comp>

這種寫法只是下面的語法糖:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

子組件內,若是修改了foo時,須要觸發update:foo事件。

v-model

v-model經常使用於相似表單這樣的自定義控件:

<my-checkbox v-model="foo"></my-checkbox>

它也是以下語法的語法糖:

<my-checkbox
  :value="foo"
  @input="val => foo = val" >
</my-checkbox>

插槽

仔細思考剛纔的自定義組件的定義,不難發現,上面的自定義組件只能對DOM中的一棵子樹作抽象和封裝。

那麼,考慮這樣一種狀況,咱們封裝了一個card組件,card的內容可使用任意的vue組件填充。
這種場景,就須要在自定義組件時,可以在組件的DOM樹裏 挖個洞 ,這個洞可以讓該組件的調用者填充。
vue提供的這種相似的機制,被稱爲插槽。

定義插槽

<div id="x-my-card">
    <h2>我是子組件的標題
        <slot name="title"></slot>
    </h2>
    <slot>
    </slot>
</div>

<script>
Vue.component('x-my-card', {
  template: '#x-my-card'
});
</script>
<div id="x-component">
    <x-my-card>
        <p>這是一些初始內容</p>
        <p>這是更多的初始內容</p>
    </x-my-card>
    <x-my-card>
          <h2 slot="title">標題</h2>
          <p>這是一些初始內容</p>
          <p>這是更多的初始內容</p>
    </x-my-card>
</div>

<script>
Vue.component('x-component', {
  template: '#x-component'
});
</script>

從上面的示例中能夠看到:

  1. 在自定義組件時,使用slot標籤給自定義組件留了一個「洞」。
  2. 在引用該自定義組件時,自定義組件標籤內部的子元素會填補上這個洞,被渲染出來。
  3. 默認的插槽只能有一個。可使用slot標籤的name屬性定義插槽名稱以區分不一樣的插槽,這樣可以在自定義組件上挖多個」洞」。

數據傳遞

vue提供的插槽機制,在給自定義組件挖」洞」的同時,還能使自定義組件給洞裏填充的組件傳遞數據。以下:

<div id="x-my-card">
  <slot text="hello from child"></slot>
</div>
<div id="x-component">
    <x-my-card>
        <template slot-scope="scope">
            <span>{{ scope.text }}</span>
        </template>
    </x-my-card>
</div>

從上面能夠看出:

  1. 在定義slot時,能夠經過屬性將數據傳遞給它。在引用自定義組件的地方,將插槽內容放入template標籤內,經過slot-scope指定變量名,便可在template標籤內引用該變量從而使用插槽傳遞過來的數據。
  2. 在實際使用中,一個典型的例子是,表格組件提供插槽自定義表格行的樣式和佈局,同時經過插槽將該表格行的數據傳遞給插槽內容。

最後

本篇博文梳理了vue的自定義組件機制,經過自定義組件,就可以在vue項目中很好的將項目組件化。
一方面,可以提取共同的組件進行復用,下降代碼冗餘;另一方面,也可以提供一種強大的抽象機制,提升vue的表達能力。

注:該文於2018-04-10撰寫於個人github靜態頁博客,現同步到個人segmentfault來。

相關文章
相關標籤/搜索