11-vue.js-組件深入了解

組件名

Vue.component('my-component-name', { /* ... */ })

該組件名就是 Vue.component 的第一個參數。

自定義組件名 (字母全小寫且必須包含一個連字符)。可以避免和當前以及未來的 HTML 元素相沖突。

組件名大小寫

定義組件名的方式有兩種:

使用 kebab-case

Vue.component('my-component-name', { /* ... */ })

引用這個自定義元素時使用 kebab-case,例如 <my-component-name>

使用 PascalCase

Vue.component('MyComponentName', { /* ... */ })

<my-component-name><MyComponentName> 都是可接受的。注意,直接在 DOM (即非字符串的模板) 中使用時只有 kebab-case 是有效的。

全局注冊

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

這些組件是全局注冊的。也就是說它們在注冊之后可以用在任何新創建的 Vue 根實例 (new Vue) 的模板中。比如:

Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

在所有子組件中也是如此,也就是說這三個組件在各自內部也都可以相互使用。

局部注冊

全局注冊往往是不夠理想的。比如,如果你使用一個像 webpack 這樣的構建系統,全局注冊所有的組件意味著即便你已經不再使用一個組件了,它仍然會被包含在你最終的構建結果中。這造成了用戶下載的 JavaScript 的無謂的增加。

在這些情況下,你可以通過一個普通的 JavaScript 對象來定義組件:

var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }

然后在 components 選項中定義你想要使用的組件:

new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

對于 components 對象中的每個屬性來說,其屬性名就是自定義元素的名字,其屬性值就是這個組件的選項對象。

局部注冊的組件在其子組件中不可用。若要 ComponentAComponentB 中可用,需要在ComponentB 中在注冊一下ComponentA 組件:

var ComponentA = { /* ... */ }

var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

或者如果你通過 Babel 和 webpack 使用 ES2015 模塊,那么代碼看起來更像:

import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA
  },
  // ...
}

模塊系統

在模塊系統中局部注冊

在局部注冊之前導入每個你想使用的組件。例如,在一個假設的 ComponentB.jsComponentB.vue 文件中:

import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}

現在 ComponentAComponentC 都可以在 ComponentB 的模板中使用了。

基礎組件的自動化全局注冊(??????????????)

可能你的許多組件只是包裹了一個輸入框或按鈕之類的元素,是相對通用的。我們有時候會把它們稱為基礎組件,它們會在各個組件中被頻繁的用到。

所以會導致很多組件里都會有一個包含基礎組件的長列表:

import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'

export default {
  components: {
    BaseButton,
    BaseIcon,
    BaseInput
  }
}

而只是用于模板中的一小部分:

<BaseInput
  v-model="searchText"
  @keydown.enter="search"
/>
<BaseButton @click="search">
  <BaseIcon name="search"/>
</BaseButton>

幸好如果你使用了 webpack (或在內部使用了 webpack 的 Vue CLI 3+),那么就可以使用 require.context 只全局注冊這些非常通用的基礎組件。這里有一份可以讓你在應用入口文件 (比如 src/main.js) 中全局導入基礎組件的示例代碼:

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其組件目錄的相對路徑
  './components',
  // 是否查詢其子目錄
  false,
  // 匹配基礎組件文件名的正則表達式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 獲取組件配置
  const componentConfig = requireComponent(fileName)

  // 獲取組件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 獲取和目錄深度無關的文件名
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )

  // 全局注冊組件
  Vue.component(
    componentName,
    // 如果這個組件選項是通過 `export default` 導出的,
    // 那么就會優先使用 `.default`,
    // 否則回退到使用模塊的根。
    componentConfig.default || componentConfig
  )
})

記住全局注冊的行為必須在根 Vue 實例 (通過 new Vue) 創建之前發生

Prop

Prop 的大小寫 (camelCase vs kebab-case)

HTML 中的特性名是大小寫不敏感的,所以瀏覽器會把所有大寫字符解釋為小寫字符。這意味著當你使用 DOM 中的模板時,camelCase (駝峰命名法) 的 prop 名需要使用其等價的 kebab-case (短橫線分隔命名) 命名

Vue.component('blog-post', {
  // 在 JavaScript 中是 camelCase 的
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>

重申一次,如果你使用字符串模板,那么這個限制就不存在了。

Prop 類型

字符串形式
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
對象形式列出 prop,這些屬性的名稱和值分別是 prop 各自的名稱和類型:
props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

傳遞靜態或動態 Prop

給 prop 傳入一個靜態的值:

<blog-post title="My journey with Vue"></blog-post>

prop 可以通過 v-bind 動態賦值,

<!-- 動態賦予一個變量的值 -->
<blog-post v-bind:title="post.title"></blog-post>

<!-- 動態賦予一個復雜表達式的值 -->
<blog-post
  v-bind:title="post.title + ' by ' + post.author.name"
></blog-post>

任何類型的值都可以傳給一個 prop。

傳入一個數字

<!-- 即便 `42` 是靜態的,我們仍然需要 `v-bind` 來告訴 Vue -->
<!-- 這是一個 JavaScript 表達式而不是一個字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一個變量進行動態賦值。-->
<blog-post v-bind:likes="post.likes"></blog-post>

傳入一個布爾值

<!-- 包含該 prop 沒有值的情況在內,都意味著 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是靜態的,我們仍然需要 `v-bind` 來告訴 Vue -->
<!-- 這是一個 JavaScript 表達式而不是一個字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一個變量進行動態賦值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>

傳入一個數組

<!-- 即便數組是靜態的,我們仍然需要 `v-bind` 來告訴 Vue -->
<!-- 這是一個 JavaScript 表達式而不是一個字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一個變量進行動態賦值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>

傳入一個對象

<!-- 即便對象是靜態的,我們仍然需要 `v-bind` 來告訴 Vue -->
<!-- 這是一個 JavaScript 表達式而不是一個字符串。-->
<blog-post
  v-bind:author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一個變量進行動態賦值。-->
<blog-post v-bind:author="post.author"></blog-post>

傳入一個對象的所有屬性

如果你想要將一個對象的所有屬性都作為 prop 傳入,你可以使用不帶參數的 v-bind(取代 v-bind:prop-name)。例如,對于一個給定的對象 post

post: {
  id: 1,
  title: 'My Journey with Vue'
}

下面的模板:

<blog-post v-bind="post"></blog-post>

等價于:

<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>

單向數據流

所有的 prop 都使得其父子 prop 之間形成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中,但是反過來則不行。這樣會防止從子組件意外改變父級組件的狀態,從而導致你的應用的數據流向難以理解。

父級組件發生更新時,子組件中所有的 prop 都將會刷新為最新的值。這意味著你不應該在一個子組件內部改變 prop。如果你這樣做了,Vue 會在瀏覽器的控制臺中發出警告。

這里有兩種常見的試圖改變一個 prop 的情形:

  1. 這個 prop 用來傳遞一個初始值;這個子組件接下來希望將其作為一個本地的 prop 數據來使用。在這種情況下,最好定義一個本地的 data 屬性并將這個 prop 用作其初始值:

    props: ['initialCounter'],
    data: function () {
      return {
        counter: this.initialCounter
      }
    }
    
  2. 這個 prop 以一種原始的值傳入且需要進行轉換。在這種情況下,最好使用這個 prop 的值來定義一個計算屬性:

    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }
    

注意在 JavaScript 中對象和數組是通過引用傳入的,所以對于一個數組或對象類型的 prop 來說,在子組件中改變這個對象或數組本身將會影響到父組件的狀態。

Prop 驗證

我們可以為組件的 prop 指定驗證要求,例如你知道的這些類型。如果有一個需求沒有被滿足,則 Vue 會在瀏覽器控制臺中警告你。這在開發一個會被別人用到的組件時尤其有幫助。

為了定制 prop 的驗證方式,你可以為 props 中的值提供一個帶有驗證需求的對象,而不是一個字符串數組。例如:

Vue.component('my-component', {
  props: {
    // 基礎的類型檢查 (`null` 和 `undefined` 會通過任何類型驗證)
    propA: Number,
    // 多個可能的類型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 帶有默認值的數字
    propD: {
      type: Number,
      default: 100
    },
    // 帶有默認值的對象
    propE: {
      type: Object,
      // 對象或數組默認值必須從一個工廠函數獲取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定義驗證函數
    propF: {
      validator: function (value) {
        // 這個值必須匹配下列字符串中的一個
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

當 prop 驗證失敗的時候,(開發環境構建版本的) Vue 將會產生一個控制臺的警告。

注意那些 prop 會在一個組件實例創建之前進行驗證,所以實例的屬性 (如 datacomputed 等) 在 defaultvalidator 函數中是不可用的。

類型檢查

type 可以是下列原生構造函數中的一個:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

type 還可以是一個自定義的構造函數,并且通過 instanceof 來進行檢查確認。例如,給定下列現成的構造函數:

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}

你可以使用:

Vue.component('blog-post', {
  props: {
    author: Person
  }
})

來驗證 author prop 的值是否是通過 new Person 創建的。

非 Prop 的特性

一個非 prop 特性是指傳向一個組件,但是該組件并沒有相應 prop 定義的特性。

因為顯式定義的 prop 適用于向一個子組件傳入信息,然而組件庫的作者并不總能預見組件會被用于怎樣的場景。這也是為什么組件可以接受任意的特性,而這些特性會被添加到這個組件的根元素上。

例如,想象一下你通過一個 Bootstrap 插件使用了一個第三方的 <bootstrap-date-input> 組件,這個插件需要在其 <input> 上用到一個 data-date-picker 特性。我們可以將這個特性添加到你的組件實例上:

<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>

然后這個 data-date-picker="activated" 特性就會自動添加到 <bootstrap-date-input> 的根元素上。

替換/合并已有的特性

想象一下 <bootstrap-date-input> 的模板是這樣的:

<input type="date" class="form-control">

為了給我們的日期選擇器插件定制一個主題,我們可能需要像這樣添加一個特別的類名:

<bootstrap-date-input
  data-date-picker="activated"
  class="date-picker-theme-dark"
></bootstrap-date-input>

在這種情況下,我們定義了兩個不同的 class 的值:

  • form-control,這是在組件的模板內設置好的
  • date-picker-theme-dark,這是從組件的父級傳入的

對于絕大多數特性來說,從外部提供給組件的值會替換掉組件內部設置好的值。所以如果傳入 type="text" 就會替換掉 type="date" 并把它破壞!慶幸的是,classstyle 特性會稍微智能一些,即兩邊的值會被合并起來,從而得到最終的值:form-control date-picker-theme-dark

禁用特性繼承

如果你希望組件的根元素繼承特性,你可以在組件的選項中設置 inheritAttrs: false。例如:

Vue.component('my-component', {
  inheritAttrs: false,
  // ...
})

這尤其適合配合實例的 $attrs 屬性使用,該屬性包含了傳遞給一個組件的特性名和特性值,例如:

{
  required: true,
  placeholder: 'Enter your username'
}

有了 inheritAttrs: false$attrs,你就可以手動決定這些特性會被賦予哪個元素。在撰寫[基礎組件]的時候是常會用到的:

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
})

注意 inheritAttrs: false 選項不會影響 styleclass 的綁定。

這個模式允許你在使用基礎組件的時候更像是使用原始的 HTML 元素,而不會擔心哪個元素是真正的根元素:

<base-input
  v-model="username"
  required
  placeholder="Enter your username"
></base-input>

自定義事件

事件名

不同于組件和 prop,事件名不存在任何自動化的大小寫轉換。而是觸發的事件名需要完全匹配監聽這個事件所用的名稱。舉個例子,如果觸發一個 camelCase 名字的事件:

this.$emit('myEvent')

則監聽這個名字的 kebab-case 版本是不會有任何效果的:

<!-- 沒有效果 -->
<my-component v-on:my-event="doSomething"></my-component>

不同于組件和 prop,事件名不會被用作一個 JavaScript 變量名或屬性名,所以就沒有理由使用 camelCase 或 PascalCase 了。并且 v-on 事件監聽器在 DOM 模板中會被自動轉換為全小寫 (因為 HTML 是大小寫不敏感的),所以 v-on:myEvent 將會變成 v-on:myevent——導致 myEvent 不可能被監聽到。

推薦始終使用 kebab-case 的事件名

自定義組件的 v-model

一個組件上的 v-model 默認會利用名為 value 的 prop 和名為 input 的事件,但是像單選框、復選框等類型的輸入控件可能會將 value 特性用于[不同的目的]。model 選項可以用來避免這樣的沖突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

現在在這個組件上使用 v-model 的時候:

<base-checkbox v-model="lovingVue"></base-checkbox>

這里的 lovingVue 的值將會傳入這個名為 checked 的 prop。同時當 <base-checkbox> 觸發一個 change 事件并附帶一個新的值的時候,這個 lovingVue 的屬性將會被更新。

注意你仍然需要在組件的 props 選項里聲明 checked 這個 prop。

將原生事件綁定到組件

在一個組件的根元素上直接監聽一個原生事件。可以使用 v-on.native 修飾符:

<base-input v-on:focus.native="onFocus"></base-input>

在有的時候這是很有用的,不過在你嘗試監聽一個類似 <input> 的非常特定的元素時,這并不是個好主意。比如上述 <base-input> 組件可能做了如下重構,所以根元素實際上是一個 <label> 元素:

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  >
</label>

這時,父級的 .native 監聽器將靜默失敗。它不會產生任何報錯,但是 onFocus 處理函數不會如你預期地被調用。

為了解決這個問題,Vue 提供了一個 $listeners 屬性,它是一個對象,里面包含了作用在這個組件上的所有監聽器。例如:

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

有了這個 $listeners 屬性,你就可以配合 v-on="$listeners" 將所有的事件監聽器指向這個組件的某個特定的子元素。對于類似 <input> 的你希望它也可以配合 v-model 工作的組件來說,為這些監聽器創建一個類似下述 inputListeners 的計算屬性通常是非常有用的:

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 將所有的對象合并為一個新對象
      return Object.assign({},
        // 我們從父級添加所有的監聽器
        this.$listeners,
        // 然后我們添加自定義監聽器,
        // 或覆寫一些監聽器的行為
        {
          // 這里確保組件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

現在 <base-input> 組件是一個完全透明的包裹器了,也就是說它可以完全像一個普通的 <input> 元素一樣使用了:所有跟它相同的特性和監聽器的都可以工作。

.sync 修飾符

在有些情況下,我們可能需要對一個 prop 進行“雙向綁定”。不幸的是,真正的雙向綁定會帶來維護上的問題,因為子組件可以修改父組件,且在父組件和子組件都沒有明顯的改動來源。

這也是為什么我們推薦以 update:myPropName 的模式觸發事件取而代之。舉個例子,在一個包含 title prop 的假設的組件中,我們可以用以下方法表達對其賦新值的意圖:

this.$emit('update:title', newTitle)

然后父組件可以監聽那個事件并根據需要更新一個本地的數據屬性。例如:

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

為了方便起見,我們為這種模式提供一個縮寫,即 .sync 修飾符:

<text-document v-bind:title.sync="doc.title"></text-document>

注意帶有 .sync 修飾符的 v-bind 不能和表達式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是無效的)。取而代之的是,你只能提供你想要綁定的屬性名,類似 v-model

當我們用一個對象同時設置多個 prop 的時候,也可以將這個 .sync 修飾符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>

這樣會把 doc 對象中的每一個屬性 (如 title) 都作為一個獨立的 prop 傳進去,然后各自添加用于更新的 v-on 監聽器。

v-bind.sync 用在一個字面量的對象上,例如 v-bind.sync=”{ title: doc.title }”,是無法正常工作的,因為在解析一個像這樣的復雜表達式的時候,有很多邊緣情況需要考慮。

插槽

在 2.6.0 中,我們為具名插槽和作用域插槽引入了一個新的統一的語法 (即 v-slot 指令)。

插槽內容

Vue 實現了一套內容分發的 API,將 <slot> 元素作為承載分發內容的出口。

它允許你像這樣合成組件:

<navigation-link url="/profile">
  Your Profile
</navigation-link>

然后你在 <navigation-link> 的模板中可能會寫為:

<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

當組件渲染的時候,<slot></slot> 將會被替換為“Your Profile”。插槽內可以包含任何模板代碼,包括 HTML:

<navigation-link url="/profile">
  <!-- 添加一個 Font Awesome 圖標 -->
  <span class="fa fa-user"></span>
  Your Profile
</navigation-link>

甚至其它的組件:

<navigation-link url="/profile">
  <!-- 添加一個圖標的組件 -->
  <font-awesome-icon name="user"></font-awesome-icon>
  Your Profile
</navigation-link>

如果 <navigation-link> 沒有包含一個 <slot> 元素,則該組件起始標簽和結束標簽之間的任何內容都會被拋棄。

編譯作用域

當你想在一個插槽中使用數據時,例如:

<navigation-link url="/profile">
  Logged in as {{ user.name }}
</navigation-link>

該插槽跟模板的其它地方一樣可以訪問相同的實例屬性 (也就是相同的“作用域”),而不能訪問 <navigation-link> 的作用域。例如 url 是訪問不到的:

<navigation-link url="/profile">
  Clicking here will send you to: {{ url }}
  <!--
  這里的 `url` 會是 undefined,因為 "/profile" 是
  _傳遞給_ <navigation-link> 的而不是
  在 <navigation-link> 組件*內部*定義的。
  -->
</navigation-link>

作為一條規則,請記住:

父級模板里的所有內容都是在父級作用域中編譯的;子模板里的所有內容都是在子作用域中編譯的。

后備內容(默認的內容)

有時為一個插槽設置具體的后備 (也就是默認的) 內容是很有用的,它只會在沒有提供內容的時候被渲染。例如在一個 <submit-button> 組件中:

<button type="submit">
  <slot></slot>
</button>

我們可能希望這個 <button> 內絕大多數情況下都渲染文本“Submit”。為了將“Submit”作為后備內容,我們可以將它放在 <slot> 標簽內:

<button type="submit">
  <slot>Submit</slot>
</button>

現在當我在一個父級組件中使用 <submit-button> 并且不提供任何插槽內容時:

<submit-button></submit-button>

后備內容“Submit”將會被渲染:

<button type="submit">
  Submit
</button>

但是如果我們提供內容:

<submit-button>
  Save
</submit-button>

則這個提供的內容將會被渲染從而取代后備內容:

<button type="submit">
  Save
</button>

具名插槽

有時我們需要多個插槽。例如對于一個帶有如下模板的 <base-layout> 組件:

<div class="container">
  <header>
    <!-- 我們希望把頁頭放這里 -->
  </header>
  <main>
    <!-- 我們希望把主要內容放這里 -->
  </main>
  <footer>
    <!-- 我們希望把頁腳放這里 -->
  </footer>
</div>

對于這樣的情況,<slot> 元素有一個特殊的特性:name。這個特性可以用來定義額外的插槽:

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

一個不帶 name<slot> 出口會帶有隱含的名字“default”。

在向具名插槽提供內容的時候,我們可以在一個 <template> 元素上使用 v-slot 指令,并以 v-slot 的參數的形式提供其名稱:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

現在 <template> 元素中的所有內容都將會被傳入相應的插槽。任何沒有被包裹在帶有 v-slot<template> 中的內容都會被視為默認插槽的內容。

然而,如果你希望更明確一些,仍然可以在一個 <template> 中包裹默認插槽的內容:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

任何一種寫法都會渲染出:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

注意 v-slot 只能添加在一個 <template>

作用域插槽

讓插槽內容能夠訪問子組件中才有的數據是很有用的。例如,設想一個帶有如下模板的 <current-user> 組件:

<span>
  <slot>{{ user.lastName }}</slot>
</span>

我們想讓它的后備內容顯示用戶的名,以取代正常情況下用戶的姓,如下:

<current-user>
  {{ user.firstName }}
</current-user>

然而上述代碼不會正常工作,因為只有 <current-user> 組件可以訪問到 user 而我們提供的內容是在父級渲染的。

為了讓 user 在父級的插槽內容可用,我們可以將 user 作為 <slot> 元素的一個特性綁定上去:

<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

綁定在 <slot> 元素上的特性被稱為插槽 prop。現在在父級作用域中,我們可以給 v-slot 帶一個值來定義我們提供的插槽 prop 的名字:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

在這個例子中,我們選擇將包含所有插槽 prop 的對象命名為 slotProps,但你也可以使用任意你喜歡的名字。

獨占默認插槽的縮寫語法

在上述情況下,當被提供的內容只有默認插槽時,組件的標簽才可以被當作插槽的模板來使用。這樣我們就可以把 v-slot 直接用在組件上:

<current-user v-slot:default="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

這種寫法還可以更簡單。就像假定未指明的內容對應默認插槽一樣,不帶參數的 v-slot 被假定對應默認插槽:

<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

注意默認插槽的縮寫語法不能和具名插槽混用,因為它會導致作用域不明確:

<!-- 無效,會導致警告 -->
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</current-user>

只要出現多個插槽,請始終為所有的插槽使用完整的基于 <template> 的語法:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</current-user>

解構插槽 Prop

作用域插槽的內部工作原理是將你的插槽內容包括在一個傳入單個參數的函數里:

function (slotProps) {
  // 插槽內容
}

這意味著 v-slot 的值實際上可以是任何能夠作為函數定義中的參數的 JavaScript 表達式。所以在支持的環境下 (單文件組件現代瀏覽器),你也可以使用 ES2015 解構來傳入具體的插槽 prop,如下:

<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

這樣可以使模板更簡潔,尤其是在該插槽提供了多個 prop 的時候。它同樣開啟了 prop 重命名等其它可能,例如將 user 重命名為 person

<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>

你甚至可以定義后備內容,用于插槽 prop 是 undefined 的情形:

<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>

動態插槽名

動態指令參數也可以用在 v-slot 上,來定義動態的插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

具名插槽的縮寫

v-slot:header 可以被重寫為 #header

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

然而,和其它指令一樣,該縮寫只在其有參數的時候才可用。這意味著以下語法是無效的:

<!-- 這樣會觸發一個警告 -->
<current-user #="{ user }">
  {{ user.firstName }}
</current-user>

使用縮寫,必須始終以明確插槽名取而代之

<current-user #default="{ user }">
  {{ user.firstName }}
</current-user>

其它示例

插槽 prop 允許我們將插槽轉換為可復用的模板,這些模板可以基于輸入的 prop 渲染出不同的內容。這在設計封裝數據邏輯同時允許父級組件自定義部分布局的可復用組件時是最有用的。

例如,我們要實現一個 <todo-list> 組件,它是一個列表且包含布局和過濾邏輯:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>

我們可以將每個 todo 作為父級組件的插槽,以此通過父級組件對其進行控制,然后將 todo 作為一個插槽 prop 進行綁定:

<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    <!--
    我們為每個 todo 準備了一個插槽,
    將 `todo` 對象作為一個插槽的 prop 傳入。
    -->
    <slot name="todo" v-bind:todo="todo">
      <!-- 后備內容 -->
      {{ todo.text }}
    </slot>
  </li>
</ul>

現在當我們使用 <todo-list> 組件的時候,我們可以選擇為 todo 定義一個不一樣的 <template> 作為替代方案,并且可以從子組件獲取數據:

<todo-list v-bind:todos="todos">
  <template v-slot:todo="{ todo }">
    <span v-if="todo.isComplete">?</span>
    {{ todo.text }}
  </template>
</todo-list>

動態組件 & 異步組件

在動態組件上使用 keep-alive

我們之前曾經在一個多標簽的界面中使用 is 特性來切換不同的組件:

<component v-bind:is="currentTabComponent"></component>

當在這些組件之間切換的時候,你有時會想保持這些組件的狀態,以避免反復重渲染導致的性能問題。例如我們來展開說一說這個多標簽界面:

Posts Archive

  • Cat Ipsum
  • Hipster Ipsum
  • Cupcake Ipsum

Click on a blog title to the left to view it.

你會注意到,如果你選擇了一篇文章,切換到 Archive 標簽,然后再切換回 Posts,是不會繼續展示你之前選擇的文章的。這是因為你每次切換新標簽的時候,Vue 都創建了一個新的 currentTabComponent 實例。

重新創建動態組件的行為通常是非常有用的,但是在這個案例中,我們更希望那些標簽的組件實例能夠被在它們第一次被創建的時候緩存下來。為了解決這個問題,我們可以用一個 <keep-alive> 元素將其動態組件包裹起來。

<!-- 失活的組件將會被緩存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

注意這個 <keep-alive> 要求被切換到的組件都有自己的名字,不論是通過組件的 name 選項還是局部/全局注冊。

異步組件

在大型應用中,我們可能需要將應用分割成小一些的代碼塊,并且只在需要的時候才從服務器加載一個模塊。為了簡化,Vue 允許你以一個工廠函數的方式定義你的組件,這個工廠函數會異步解析你的組件定義。Vue 只有在這個組件需要被渲染的時候才會觸發該工廠函數,且會把結果緩存起來供未來重渲染。例如:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回調傳遞組件定義
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

如你所見,這個工廠函數會收到一個 resolve 回調,這個回調函數會在你從服務器得到組件定義的時候被調用。你也可以調用 reject(reason) 來表示加載失敗。這里的 setTimeout 是為了演示用的,如何獲取組件取決于你自己。一個推薦的做法是將異步組件和 webpack 的 code-splitting 功能一起配合使用:

Vue.component('async-webpack-example', function (resolve) {
  // 這個特殊的 `require` 語法將會告訴 webpack
  // 自動將你的構建代碼切割成多個包,這些包
  // 會通過 Ajax 請求加載
  require(['./my-async-component'], resolve)
})

你也可以在工廠函數中返回一個 Promise,所以把 webpack 2 和 ES2015 語法加在一起,我們可以寫成這樣:

Vue.component(
  'async-webpack-example',
  // 這個 `import` 函數會返回一個 `Promise` 對象。
  () => import('./my-async-component')
)

當使用局部注冊的時候,你也可以直接提供一個返回 Promise 的函數:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})

處理加載狀態

這里的異步組件工廠函數也可以返回一個如下格式的對象:

const AsyncComponent = () => ({
  // 需要加載的組件 (應該是一個 `Promise` 對象)
  component: import('./MyComponent.vue'),
  // 異步組件加載時使用的組件
  loading: LoadingComponent,
  // 加載失敗時使用的組件
  error: ErrorComponent,
  // 展示加載時組件的延時時間。默認值是 200 (毫秒)
  delay: 200,
  // 如果提供了超時時間且組件加載也超時了,
  // 則使用加載失敗時使用的組件。默認值是:`Infinity`
  timeout: 3000
})

注意如果你希望在 Vue Router 的路由組件中使用上述語法的話,你必須使用 Vue Router 2.4.0+ 版本。

訪問根實例

在每個 new Vue 實例的子組件中,其根實例可以通過 $root 屬性進行訪問。例如,在這個根實例中:

// Vue 根實例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})

所有的子組件都可以將這個實例作為一個全局 store 來訪問或使用。

// 獲取根組件的數據
this.$root.foo

// 寫入根組件的數據
this.$root.foo = 2

// 訪問根組件的計算屬性
this.$root.bar

// 調用根組件的方法
this.$root.baz()

對于 demo 或非常小型的有少量組件的應用來說這是很方便的。不過這個模式擴展到中大型應用來說就不然了。因此在絕大多數情況下,我們強烈推薦使用 Vuex 來管理應用的狀態。

訪問父級組件實例

$root 類似,$parent 屬性可以用來從一個子組件訪問父組件的實例。它提供了一種機會,可以在后期隨時觸達父級組件,以替代將數據以 prop 的方式傳入子組件的方式。

在絕大多數情況下,觸達父級組件會使得你的應用更難調試和理解,尤其是當你變更了父級組件的數據的時候。當我們稍后回看那個組件的時候,很難找出那個變更是從哪里發起的。

另外在一些可能適當的時候,你需要特別地共享一些組件庫。舉個例子,在和 JavaScript API 進行交互而不渲染 HTML 的抽象組件內,諸如這些假設性的 Google 地圖組件一樣:

<google-map>
  <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>

這個 <google-map> 組件可以定義一個 map 屬性,所有的子組件都需要訪問它。在這種情況下 <google-map-markers> 可能想要通過類似 this.$parent.getMap 的方式訪問那個地圖,以便為其添加一組標記。你可以在這里查閱這種模式。

請留意,盡管如此,通過這種模式構建出來的那個組件的內部仍然是容易出現問題的。比如,設想一下我們添加一個新的 <google-map-region> 組件,當 <google-map-markers> 在其內部出現的時候,只會渲染那個區域內的標記:

<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map>

那么在 <google-map-markers> 內部你可能發現自己需要一些類似這樣的 hack:

var map = this.$parent.map || this.$parent.$parent.map

很快它就會失控。這也是我們針對需要向任意更深層級的組件提供上下文信息時推薦依賴注入的原因。

訪問子組件實例或子元素

盡管存在 prop 和事件,有的時候你仍可能需要在 JavaScript 里直接訪問一個子組件。為了達到這個目的,你可以通過 ref 特性為這個子組件賦予一個 ID 引用。例如:

<base-input ref="usernameInput"></base-input>

現在在你已經定義了這個 ref 的組件里,你可以使用:

this.$refs.usernameInput

來訪問這個 <base-input> 實例,以便不時之需。比如程序化地從一個父級組件聚焦這個輸入框。在剛才那個例子中,該 <base-input> 組件也可以使用一個類似的 ref 提供對內部這個指定元素的訪問,例如:

<input ref="input">

甚至可以通過其父級組件定義方法:

methods: {
  // 用來從父級組件聚焦輸入框
  focus: function () {
    this.$refs.input.focus()
  }
}

這樣就允許父級組件通過下面的代碼聚焦 <base-input> 里的輸入框:

this.$refs.usernameInput.focus()

refv-for 一起使用的時候,你得到的引用將會是一個包含了對應數據源的這些子組件的數組。

$refs 只會在組件渲染完成之后生效,并且它們不是響應式的。這僅作為一個用于直接操作子組件的“逃生艙”——你應該避免在模板或計算屬性中訪問 $refs

程序化的事件偵聽器

現在,你已經知道了 $emit 的用法,它可以被 v-on 偵聽,但是 Vue 實例同時在其事件接口中提供了其它的方法。我們可以:

  • 通過 $on(eventName, eventHandler) 偵聽一個事件
  • 通過 $once(eventName, eventHandler) 一次性偵聽一個事件
  • 通過 $off(eventName, eventHandler) 停止偵聽一個事件

你通常不會用到這些,但是當你需要在一個組件實例上手動偵聽事件時,它們是派得上用場的。它們也可以用于代碼組織工具。例如,你可能經常看到這種集成一個第三方庫的模式:

// 一次性將這個日期選擇器附加到一個輸入框上
// 它會被掛載到 DOM 上。
mounted: function () {
  // Pikaday 是一個第三方日期選擇器的庫
  this.picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// 在組件被銷毀之前,
// 也銷毀這個日期選擇器。
beforeDestroy: function () {
  this.picker.destroy()
}

這里有兩個潛在的問題:

  • 它需要在這個組件實例中保存這個 picker,如果可以的話最好只有生命周期鉤子可以訪問到它。這并不算嚴重的問題,但是它可以被視為雜物。
  • 我們的建立代碼獨立于我們的清理代碼,這使得我們比較難于程序化地清理我們建立的所有東西。

你應該通過一個程序化的偵聽器解決這兩個問題:

mounted: function () {
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })

  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}

使用了這個策略,我甚至可以讓多個輸入框元素同時使用不同的 Pikaday,每個新的實例都程序化地在后期清理它自己:

mounted: function () {
  this.attachDatepicker('startDateInput')
  this.attachDatepicker('endDateInput')
},
methods: {
  attachDatepicker: function (refName) {
    var picker = new Pikaday({
      field: this.$refs[refName],
      format: 'YYYY-MM-DD'
    })

    this.$once('hook:beforeDestroy', function () {
      picker.destroy()
    })
  }
}

注意 Vue 的事件系統不同于瀏覽器的 EventTarget API。盡管它們工作起來是相似的,但是 $emit$on, 和 $off 并不是 dispatchEventaddEventListenerremoveEventListener 的別名。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容

  • 什么是組件? 組件 (Component) 是 Vue.js 最強大的功能之一。組件可以擴展 HTML 元素,封裝...
    youins閱讀 9,517評論 0 13
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內容,還有我對于 Vue 1.0 印象不深的內容。關于...
    云之外閱讀 5,070評論 0 29
  • 主要還是自己看的,所有內容來自官方文檔。 介紹 Vue.js 是什么 Vue (讀音 /vju?/,類似于 vie...
    Leonzai閱讀 3,369評論 0 25
  • 組件注冊 組件名 在注冊一個組件的時候,我們始終需要給它一個名字。 該組件名就是Vue.component的第一個...
    oWSQo閱讀 404評論 0 1
  • 組件(Component)是Vue.js最核心的功能,也是整個架構設計最精彩的地方,當然也是最難掌握的。...
    六個周閱讀 5,638評論 0 32