vue.js 組件

什么是組件


組件(Component)是 Vue.js 最強大的功能之一。組件可以擴展 HTML 元素,封裝可重用的代碼。在較高層面上,組件是自定義元素, Vue.js 的編譯器為它添加特殊功能。在有些情況下,組件也可以是原生 HTML 元素的形式,以 is 特性擴展。

使用組件


# 注冊

我們可以通過以下方式創(chuàng)建一個 Vue 實例:

    new Vue({
      el: '#some-element',
      // 選項
    })

要注冊一個全局組件,可以使用 Vue.component(tagName, options)。 例如:

    Vue.component('my-component', {
      // 選項
    })

對于自定義標簽名,Vue.js 不強制要求遵循 W3C規(guī)則 (小寫,并且包含一個短杠),盡管遵循這個規(guī)則比較好。

組件在注冊之后,便可以在父實例的模塊中以自定義元素 <my-component></my-component> 的形式使用。要確保在初始化根實例 之前 注冊了組件:

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


    // 注冊
    Vue.component('my-component', {
      template: '<div>A custom component!</div>'
    })
    // 創(chuàng)建根實例
    new Vue({
      el: '#example'
    })

渲染為:

    <div id="example">
      <div>A custom component!</div>
    </div>

# 局部注冊

不必在全局注冊每個組件。通過使用組件實例選項注冊,可以使組件僅在另一個實例/組件的作用域中可用:

    var Child = {
      template: '<div>A custom component!</div>'
    }

    new Vue({
      // ...
      components: {
        // <my-component> 將只在父模板可用
        'my-component': Child
      }
    })

# DOM模板解析說明

當使用 DOM 作為模版時(例如,將 el 選項掛載到一個已存在的元素上), 你會受到 HTML 的一些限制,因為 Vue 只有在瀏覽器解析和標準化 HTML 后才能獲取模版內(nèi)容。尤其像這些元素 <ul> , <ol>, <table> , <select> 限制了能被它包裹的元素, <option> 只能出現(xiàn)在其它元素內(nèi)部。

在自定義組件中使用這些受限制的元素時會導致一些問題,例如:

    <table>
      <my-row>...</my-row>
    </table>

自定義組件 <my-row>被認為是無效的內(nèi)容,因此在渲染的時候會導致錯誤。變通的方案是使用特殊的 is 屬性:

    <table>
      <tr is="my-row"></tr>
    </table>

應當注意,如果您使用來自以下來源之一的字符串模板,這些限制將不適用:

  • <script type="text/x-template">
  • JavaScript內(nèi)聯(lián)模版字符串
  • .vue 組件

# data 必須是函數(shù)

使用組件時,大多數(shù)選項可以被傳入到 Vue 構(gòu)造器中,有一個例外: data 必須是函數(shù)。 實際上,如果你這么做:

    Vue.component('my-component', {
      template: '<span>{{ message }}</span>',
      data: {
        message: 'hello'
      }
    })

那么 Vue 會在控制臺發(fā)出警告,告訴你在組件中 data 必須是一個函數(shù)。最好理解這種規(guī)則的存在意義。

    <div id="example-2">
      <simple-counter></simple-counter>
      <simple-counter></simple-counter>
      <simple-counter></simple-counter>
    </div>

    var data = { counter: 0 }

    Vue.component('simple-counter', {
      template: '<button v-on:click="counter += 1">{{ counter }}</button>',
      // data 是一個函數(shù),因此 Vue 不會警告,
      // 但是我們?yōu)槊恳粋€組件返回了同一個對象引用
      data: function () {
        return data
      }
    })
    new Vue({
      el: '#example-2'
    })

由于這三個組件共享了同一個 data , 因此增加一個 counter 會影響所有組件!我們可以通過為每個組件返回新的 data 對象來解決這個問題:

    data: function () {
      return {
        counter: 0
      }
    }

現(xiàn)在每個 counter 都有它自己內(nèi)部的狀態(tài)了.

# 組件構(gòu)成

組件意味著協(xié)同工作,通常父子組件會是這樣的關(guān)系:組件 A 在它的模版中使用了組件 B 。它們之間必然需要相互通信:父組件要給子組件傳遞數(shù)據(jù),子組件需要將它內(nèi)部發(fā)生的事情告知給父組件。然而,在一個良好定義的接口中盡可能將父子組件解耦是很重要的。這保證了每個組件可以在相對隔離的環(huán)境中書寫和理解,也大幅提高了組件的可維護性和可重用性。

在 Vue.js 中,父子組件的關(guān)系可以總結(jié)為 props down, events up 。父組件通過 props 向下傳遞數(shù)據(jù)給子組件,子組件通過 events 給父組件發(fā)送消息。

Prop


# 使用Prop傳遞數(shù)據(jù)

組件實例的作用域是孤立的。這意味著不能并且不應該在子組件的模板內(nèi)直接引用父組件的數(shù)據(jù)??梢允褂?props 把數(shù)據(jù)傳給子組件。

prop 是父組件用來傳遞數(shù)據(jù)的一個自定義屬性。子組件需要顯式地用 props 選項聲明 “prop”:

    Vue.component('child', {
      // 聲明 props
      props: ['message'],
      // 就像 data 一樣,prop 可以用在模板內(nèi)
      // 同樣也可以在 vm 實例中像 “this.message” 這樣使用
      template: '<span>{{ message }}</span>'
    })

然后向它傳入一個普通字符串:

<child message="hello!"></child>

結(jié)果:

Hello!

# camelCase vs. kebab-case

HTML 特性不區(qū)分大小寫。當使用非字符串模版時,prop的名字形式會從 camelCase 轉(zhuǎn)為 kebab-case(短橫線隔開):

    Vue.component('child', {
      // camelCase in JavaScript
      props: ['myMessage'],
      template: '<span>{{ myMessage }}</span>'
    })
    <!-- kebab-case in HTML -->
    <child my-message="hello!"></child>

再次說明,如果你使用字符串模版,不用在意這些限制。

# 動態(tài)Prop

類似于用 v-bind 綁定 HTML 特性到一個表達式,也可以用 v-bind動態(tài)綁定 props 的值到父組件的數(shù)據(jù)中。每當父組件的數(shù)據(jù)變化時,該變化也會傳導給子組件:

    <div>
      <input v-model="parentMsg">
      <br>
      <child v-bind:my-message="parentMsg"></child>
    </div>

使用 v-bind 的縮寫語法通常更簡單:

    <child :my-message="parentMsg"></child>

# 字面量語法 VS 動態(tài)語法

初學者常犯的一個錯誤是使用字面量語法傳遞數(shù)值:

    <!-- 傳遞了一個字符串"1" -->
    <comp some-prop="1"></comp>

因為它是一個字面 prop ,它的值以字符串 "1" 而不是以實際的數(shù)字傳下去。如果想傳遞一個實際的 JavaScript 數(shù)字,需要使用 v-bind ,從而讓它的值被當作 JavaScript 表達式計算:

    <!-- 傳遞實際的數(shù)字 -->
    <comp v-bind:some-prop="1"></comp>

# 單向數(shù)據(jù)流

prop 是單向綁定的:當父組件的屬性變化時,將傳導給子組件,但是不會反過來。這是為了防止子組件無意修改了父組件的狀態(tài)——這會讓應用的數(shù)據(jù)流難以理解。

另外,每次父組件更新時,子組件的所有 prop 都會更新為最新值。這意味著你不應該在子組件內(nèi)部改變 prop 。如果你這么做了,Vue 會在控制臺給出警告。

通常有兩種改變 prop 的情況:

  • prop 作為初始值傳入,子組件之后只是將它的初始值作為本地數(shù)據(jù)的初始值使用;
  • prop 作為需要被轉(zhuǎn)變的原始值傳入。

更確切的說這兩種情況是:

  1. 定義一個局部 data 屬性,并將 prop 的初始值作為局部數(shù)據(jù)的初始值。
    props: ['initialCounter'],
    data: function () {
      return { counter: this.initialCounter }
    }
  1. 定義一個 computed 屬性,此屬性從 prop 的值計算得出。
    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }

注意在 JavaScript 中對象和數(shù)組是引用類型,指向同一個內(nèi)存空間,如果 prop 是一個對象或數(shù)組,在子組件內(nèi)部改變它會影響父組件的狀態(tài)。

# Prop驗證

組件可以為 props 指定驗證要求。如果未指定驗證要求,Vue 會發(fā)出警告。當組件給其他人使用時這很有用。

prop 是一個對象而不是字符串數(shù)組時,它包含驗證要求:

    Vue.component('example', {
      props: {
        // 基礎類型檢測 (`null` 意思是任何類型都可以)
        propA: Number,
        // 多種類型
        propB: [String, Number],
        // 必傳且是字符串
        propC: {
          type: String,
          required: true
        },
        // 數(shù)字,有默認值
        propD: {
          type: Number,
          default: 100
        },
        // 數(shù)組/對象的默認值應當由一個工廠函數(shù)返回
        propE: {
          type: Object,
          default: function () {
            return { message: 'hello' }
          }
        },
        // 自定義驗證函數(shù)
        propF: {
          validator: function (value) {
            return value > 10
          }
        }
      }
    })

type 可以是下面原生構(gòu)造器:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array

type 也可以是一個自定義構(gòu)造器,使用 instanceof 檢測。

當 prop 驗證失敗了, Vue 將拒絕在子組件上設置此值,如果使用的是開發(fā)版本會拋出一條警告。

自定義事件


我們知道,父組件是使用 props 傳遞數(shù)據(jù)給子組件,但如果子組件要把數(shù)據(jù)傳遞回去,應該怎樣做?那就是自定義事件!

# 使用 v-on 綁定事件

每個 Vue 實例都實現(xiàn)了事件接口(Events interface),即:

  • 使用 $on(eventName) 監(jiān)聽事件
  • 使用 $emit(eventName) 觸發(fā)事件

Vue的事件系統(tǒng)分離自瀏覽器的EventTarget API。盡管它們的運行類似,但是$on$emit 不是addEventListenerdispatchEvent 的別名。

另外,父組件可以在使用子組件的地方直接用 v-on 來監(jiān)聽子組件觸發(fā)的事件。

下面是一個例子:

    <div id="counter-event-example">
      <p>{{ total }}</p>
      <button-counter v-on:increment="incrementTotal"></button-counter>
      <button-counter v-on:increment="incrementTotal"></button-counter>
    </div>

監(jiān)聽子組件increment自定義事件,觸發(fā)時調(diào)用父元素incrementTotal方法

    Vue.component('button-counter', {
      template: '<button v-on:click="increment">{{ counter }}</button>',
      data: function () {
        return {
          counter: 0
        }
      },
      methods: {
        increment: function () {
          this.counter += 1
          this.$emit('increment')//觸發(fā)increment自定義事件,傳遞給父元素
        }
      },
    })
    new Vue({
      el: '#counter-event-example',
      data: {
        total: 0
      },
      methods: {
        incrementTotal: function () {
          this.total += 1
        }
      }
    })

給組件綁定原生事件

有時候,你可能想在某個組件的根元素上監(jiān)聽一個原生事件??梢允褂?.native 修飾 v-on 。例如:

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

# 使用自定義事件的表單輸入組件

自定義事件也可以用來創(chuàng)建自定義的表單輸入組件,使用 v-model 來進行數(shù)據(jù)雙向綁定。牢記:

    <input v-model="something">

僅僅是一個語法糖:

    <input v-bind:value="something" v-on:input="something = $event.target.value">

所以在組件中使用時,它相當于下面的簡寫:

    <custom-input v-bind:value="something" v-on:input="something = arguments[0]"></custom-input>

所以要讓組件的 v-model 生效,它必須:

  • 接受一個 value 屬性
  • 在有新的 value 時觸發(fā) input 事件

一個非常簡單的貨幣輸入:

<currency-input v-model="price"></currency-input>
    Vue.component('currency-input', {
      template: '\
        <span>\
          $\
          <input\
            ref="input"\
            v-bind:value="value"\
            v-on:input="updateValue($event.target.value)"\
          >\
        </span>\
      ',
      props: ['value'],
      methods: {
        // 不是直接更新值,而是使用此方法來對輸入值進行格式化和位數(shù)限制
        updateValue: function (value) {
          var formattedValue = value
            // 刪除兩側(cè)的空格符
            .trim()
            // 保留 2 小數(shù)位
            .slice(0, value.indexOf('.') + 3)
          // 如果值不統(tǒng)一,手動覆蓋以保持一致
          if (formattedValue !== value) {
            this.$refs.input.value = formattedValue
          }
          // 通過 input 事件發(fā)出數(shù)值
          this.$emit('input', Number(formattedValue))
        }
      }
    })

# 非父子組件通信

有時候非父子關(guān)系的組件也需要通信。在簡單的場景下,使用一個空的 Vue 實例作為中央事件總線:

    var bus = new Vue()
    // 觸發(fā)組件 A 中的事件
    bus.$emit('id-selected', 1)
    // 在組件 B 創(chuàng)建的鉤子中監(jiān)聽事件
    bus.$on('id-selected', function (id) {
      // ...
    })

使用slot分發(fā)內(nèi)容


在使用組件時,常常要像這樣組合它們:

<app>
  <app-header></app-header>
  <app-footer></app-footer>
</app>

注意兩點:

  1. <app> 組件不知道它的掛載點會有什么內(nèi)容。掛載點的內(nèi)容是由<app>的父組件決定的。
  2. '<app>' 組件很可能有它自己的模版。

為了讓組件可以組合,我們需要一種方式來混合父組件的內(nèi)容與子組件自己的模板。這個過程被稱為 內(nèi)容分發(fā) (或 “transclusion” 如果你熟悉 Angular)。Vue.js 實現(xiàn)了一個內(nèi)容分發(fā) API ,參照了當前 Web 組件規(guī)范草案,使用特殊的 '<slot>' 元素作為原始內(nèi)容的插槽。

# 編譯作用域

在深入內(nèi)容分發(fā) API 之前,我們先明確內(nèi)容的編譯作用域。假定模板為:

<child-component>
  {{ message }}
</child-component>

message 應該綁定到父組件的數(shù)據(jù),還是綁定到子組件的數(shù)據(jù)?答案是父組件。組件作用域簡單地說是:

父組件模板的內(nèi)容在父組件作用域內(nèi)編譯;子組件模板的內(nèi)容在子組件作用域內(nèi)編譯。

一個常見錯誤是試圖在父組件模板內(nèi)將一個指令綁定到子組件的屬性/方法:

    <!-- 無效 -->
    <child-component v-show="someChildProperty"></child-component>

假定 'someChildProperty '是子組件的屬性,上例不會如預期那樣工作。父組件模板不應該知道子組件的狀態(tài)。

如果要綁定子組件內(nèi)的指令到一個組件的根節(jié)點,應當在它的模板內(nèi)這么做:

Vue.component('child-component', {
  // 有效,因為是在正確的作用域內(nèi)
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

類似地,分發(fā)內(nèi)容是在父組件作用域內(nèi)編譯。

# 單個slot

除非子組件模板包含至少一個 <slot> 插口,否則父組件的內(nèi)容將會被丟棄。當子組件模板只有一個沒有屬性的 slot 時,父組件整個內(nèi)容片段將插入到 slot 所在的 DOM 位置,并替換掉 slot 標簽本身。

最初在 <slot> 標簽中的任何內(nèi)容都被視為備用內(nèi)容。備用內(nèi)容在子組件的作用域內(nèi)編譯,并且只有在宿主元素為空,且沒有要插入的內(nèi)容時才顯示備用內(nèi)容。

假定 my-component 組件有下面模板:

div>
  <h2>我是子組件的標題</h2>
  <slot>
    只有在沒有要分發(fā)的內(nèi)容時才會顯示。
  </slot>
</div>

父組件模版:

<div>
  <h1>我是父組件的標題</h1>
  <my-component>
    <p>這是一些初始內(nèi)容</p>
    <p>這是更多的初始內(nèi)容</p>
  </my-component>
</div>

渲染結(jié)果:

<div>
  <h1>我是父組件的標題</h1>
  <div>
    <h2>我是子組件的標題</h2>
    <p>這是一些初始內(nèi)容</p>
    <p>這是更多的初始內(nèi)容</p>
  </div>
</div>

# 具名slot

<slot> 元素可以用一個特殊的屬性 name 來配置如何分發(fā)內(nèi)容。多個 slot 可以有不同的名字。具名 slot 將匹配內(nèi)容片段中有對應 slot 特性的元素。

仍然可以有一個匿名 slot ,它是默認 slot ,作為找不到匹配的內(nèi)容片段的備用插槽。如果沒有默認的 slot ,這些找不到匹配的內(nèi)容片段將被拋棄。

例如,假定我們有一個 app-layout 組件,它的模板為:

    <div class="container">
          <header>
             <slot name="header"></slot>
          </header>
          <main>
             <slot></slot>
          </main>
          <footer>
             <slot name="footer"></slot>
          </footer>
    </div>

父組件模版:

    <app-layout>
      <h1 slot="header">這里可能是一個頁面標題</h1>
      <p>主要內(nèi)容的一個段落。</p>
      <p>另一個主要段落。</p>
      <p slot="footer">這里有一些聯(lián)系信息</p>
    </app-layout>

渲染結(jié)果為:

    div class="container">
      <header>
        <h1>這里可能是一個頁面標題</h1>
      </header>
      <main>
        <p>主要內(nèi)容的一個段落。</p>
        <p>另一個主要段落。</p>
      </main>
      <footer>
        <p>這里有一些聯(lián)系信息</p>
      </footer>
    </div>

# 作用域插槽

2.1 新增

作用域插槽是一種特殊類型的插槽,用作使用一個(能夠傳遞數(shù)據(jù)到)可重用模板替換已渲染元素。

在子組件中,只需將數(shù)據(jù)傳遞到插槽,就像你將 prop 傳遞給組件一樣:

    <div class="child">
      <slot text="hello from child"></slot>
    </div>

在父級中,具有特殊屬性 scope<template> 元素,表示它是作用域插槽的模板。scope 的值對應一個臨時變量名,此變量接收從子組件中傳遞的 prop 對象:

    <div class="parent">
      <child>
        <template scope="props">
          <span>hello from parent</span>
          <span>{{ props.text }}</span>
        </template>
      </child>
    </div>

如果我們渲染以上結(jié)果,得到的輸出會是:

    <div class="parent">
      <div class="child">
        <span>hello from parent</span>
        <span>hello from child</span>
      </div>
    </div>

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

    <my-awesome-list :items="items">
      <!-- 作用域插槽也可以在這里命名 -->
      <template slot="item" scope="props">
        <li class="my-fancy-item">{{ props.text }}</li>
      </template>
    </my-awesome-list>

列表組件的模板:

    <ul>
      <slot name="item"
        v-for="item in items" :text="item.text">
        <!-- fallback content here -->
      </slot>
    </ul>

動態(tài)組件

多個組件可以使用同一個掛載點,然后動態(tài)地在它們之間切換。使用保留的 <component> 元素,動態(tài)地綁定到它的 is 特性:

  var vm = new Vue({
    el: '#example',
    data: {
      currentView: 'home'
    },
    components: {
      home: { /* ... */ },
      posts: { /* ... */ },
      archive: { /* ... */ }
    }
  })
  component v-bind:is="currentView">
    <!-- 組件在 vm.currentview 變化時改變! -->
  </component>

也可以直接綁定到組件對象上:

  var Home = {
    template: '<p>Welcome home!</p>'
  }
  var vm = new Vue({
    el: '#example',
    data: {
      currentView: Home
    }
  })

# keep-alive

如果把切換出去的組件保留在內(nèi)存中,可以保留它的狀態(tài)或避免重新渲染。為此可以添加一個 keep-alive 指令參數(shù):

  <keep-alive>
    <component :is="currentView">
      <!-- 非活動組件將被緩存! -->
    </component>
  </keep-alive>

API參考查看更多 <keep-alive> 的細節(jié)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文章是我最近在公司的一場內(nèi)部分享的內(nèi)容。我有個習慣就是每次分享都會先將要分享的內(nèi)容寫成文章。所以這個文集也是用來...
    Awey閱讀 9,573評論 4 67
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容,還有我對于 Vue 1.0 印象不深的內(nèi)容。關(guān)于...
    云之外閱讀 5,173評論 0 29
  • 有一篇非常棒的關(guān)于vue.js的組件的文章,寫的特別好,特別清楚,容易理解。鏈接:上篇:http://www.cn...
    恰皮閱讀 1,861評論 0 8
  • 此文基于官方文檔,里面部分例子有改動,加上了一些自己的理解 什么是組件? 組件(Component)是 Vue.j...
    陸志均閱讀 3,944評論 5 14
  • 從成吉思汗的雕像 到隴西絕美的風光 從桂林的喀斯特地貌 到湘西小鎮(zhèn)的廣場 我在路上 從西藏那曲遼闊的草場 到昆侖山...
    貨車司機牛二哥閱讀 200評論 0 3

友情鏈接更多精彩內(nèi)容