組件名
Vue.component('my-component-name', { /* ... */ })
該組件名就是 Vue.component
的第一個參數(shù)。
自定義組件名 (字母全小寫且必須包含一個連字符)骄蝇∩乓螅可以避免和當(dāng)前以及未來的 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', {
// ... 選項 ...
})
這些組件是全局注冊的跨细。也就是說它們在注冊之后可以用在任何新創(chuàng)建的 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>
在所有子組件中也是如此河质,也就是說這三個組件在各自內(nèi)部也都可以相互使用冀惭。
局部注冊
全局注冊往往是不夠理想的。比如掀鹅,如果你使用一個像 webpack 這樣的構(gòu)建系統(tǒng)散休,全局注冊所有的組件意味著即便你已經(jīng)不再使用一個組件了,它仍然會被包含在你最終的構(gòu)建結(jié)果中乐尊。這造成了用戶下載的 JavaScript 的無謂的增加戚丸。
在這些情況下,你可以通過一個普通的 JavaScript 對象來定義組件:
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
然后在 components
選項中定義你想要使用的組件:
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
對于 components
對象中的每個屬性來說扔嵌,其屬性名就是自定義元素的名字限府,其屬性值就是這個組件的選項對象碎紊。
局部注冊的組件在其子組件中不可用昂验。若要
ComponentA
在ComponentB
中可用艺蝴,需要在ComponentB
中在注冊一下ComponentA
組件:
var ComponentA = { /* ... */ }
var ComponentB = {
components: {
'component-a': ComponentA
},
// ...
}
或者如果你通過 Babel 和 webpack 使用 ES2015 模塊昨登,那么代碼看起來更像:
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
},
// ...
}
模塊系統(tǒng)
在模塊系統(tǒng)中局部注冊
在局部注冊之前導(dǎo)入每個你想使用的組件叽赊。例如蚌讼,在一個假設(shè)的 ComponentB.js
或 ComponentB.vue
文件中:
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'
export default {
components: {
ComponentA,
ComponentC
},
// ...
}
現(xiàn)在 ComponentA
和 ComponentC
都可以在 ComponentB
的模板中使用了芯义。
基礎(chǔ)組件的自動化全局注冊(??????????????)
可能你的許多組件只是包裹了一個輸入框或按鈕之類的元素趁舀,是相對通用的嵌洼。我們有時候會把它們稱為基礎(chǔ)組件,它們會在各個組件中被頻繁的用到案疲。
所以會導(dǎo)致很多組件里都會有一個包含基礎(chǔ)組件的長列表:
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 (或在內(nèi)部使用了 webpack 的 Vue CLI 3+),那么就可以使用 require.context
只全局注冊這些非常通用的基礎(chǔ)組件麻养。這里有一份可以讓你在應(yīng)用入口文件 (比如 src/main.js
) 中全局導(dǎo)入基礎(chǔ)組件的示例代碼:
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
const requireComponent = require.context(
// 其組件目錄的相對路徑
'./components',
// 是否查詢其子目錄
false,
// 匹配基礎(chǔ)組件文件名的正則表達式
/Base[A-Z]\w+\.(vue|js)$/
)
requireComponent.keys().forEach(fileName => {
// 獲取組件配置
const componentConfig = requireComponent(fileName)
// 獲取組件的 PascalCase 命名
const componentName = upperFirst(
camelCase(
// 獲取和目錄深度無關(guān)的文件名
fileName
.split('/')
.pop()
.replace(/\.\w+$/, '')
)
)
// 全局注冊組件
Vue.component(
componentName,
// 如果這個組件選項是通過 `export default` 導(dǎo)出的络拌,
// 那么就會優(yōu)先使用 `.default`,
// 否則回退到使用模塊的根回溺。
componentConfig.default || componentConfig
)
})
記住全局注冊的行為必須在根 Vue 實例 (通過 new Vue
) 創(chuàng)建之前發(fā)生春贸。
Prop
Prop 的大小寫 (camelCase vs kebab-case)
HTML 中的特性名是大小寫不敏感的,所以瀏覽器會把所有大寫字符解釋為小寫字符遗遵。這意味著當(dāng)你使用 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
}
傳遞靜態(tài)或動態(tài) Prop
給 prop 傳入一個靜態(tài)的值:
<blog-post title="My journey with Vue"></blog-post>
prop 可以通過 v-bind
動態(tài)賦值,
<!-- 動態(tài)賦予一個變量的值 -->
<blog-post v-bind:title="post.title"></blog-post>
<!-- 動態(tài)賦予一個復(fù)雜表達式的值 -->
<blog-post
v-bind:title="post.title + ' by ' + post.author.name"
></blog-post>
任何類型的值都可以傳給一個 prop类垫。
傳入一個數(shù)字
<!-- 即便 `42` 是靜態(tài)的司光,我們?nèi)匀恍枰?`v-bind` 來告訴 Vue -->
<!-- 這是一個 JavaScript 表達式而不是一個字符串。-->
<blog-post v-bind:likes="42"></blog-post>
<!-- 用一個變量進行動態(tài)賦值悉患。-->
<blog-post v-bind:likes="post.likes"></blog-post>
傳入一個布爾值
<!-- 包含該 prop 沒有值的情況在內(nèi)残家,都意味著 `true`。-->
<blog-post is-published></blog-post>
<!-- 即便 `false` 是靜態(tài)的售躁,我們?nèi)匀恍枰?`v-bind` 來告訴 Vue -->
<!-- 這是一個 JavaScript 表達式而不是一個字符串坞淮。-->
<blog-post v-bind:is-published="false"></blog-post>
<!-- 用一個變量進行動態(tài)賦值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>
傳入一個數(shù)組
<!-- 即便數(shù)組是靜態(tài)的陪捷,我們?nèi)匀恍枰?`v-bind` 來告訴 Vue -->
<!-- 這是一個 JavaScript 表達式而不是一個字符串回窘。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>
<!-- 用一個變量進行動態(tài)賦值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>
傳入一個對象
<!-- 即便對象是靜態(tài)的市袖,我們?nèi)匀恍枰?`v-bind` 來告訴 Vue -->
<!-- 這是一個 JavaScript 表達式而不是一個字符串啡直。-->
<blog-post
v-bind:author="{
name: 'Veronica',
company: 'Veridian Dynamics'
}"
></blog-post>
<!-- 用一個變量進行動態(tài)賦值。-->
<blog-post v-bind:author="post.author"></blog-post>
傳入一個對象的所有屬性
如果你想要將一個對象的所有屬性都作為 prop 傳入苍碟,你可以使用不帶參數(shù)的 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>
單向數(shù)據(jù)流
所有的 prop 都使得其父子 prop 之間形成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中驰怎,但是反過來則不行。這樣會防止從子組件意外改變父級組件的狀態(tài)二打,從而導(dǎo)致你的應(yīng)用的數(shù)據(jù)流向難以理解县忌。
父級組件發(fā)生更新時,子組件中所有的 prop 都將會刷新為最新的值继效。這意味著你不應(yīng)該在一個子組件內(nèi)部改變 prop症杏。如果你這樣做了,Vue 會在瀏覽器的控制臺中發(fā)出警告瑞信。
這里有兩種常見的試圖改變一個 prop 的情形:
-
這個 prop 用來傳遞一個初始值厉颤;這個子組件接下來希望將其作為一個本地的 prop 數(shù)據(jù)來使用。在這種情況下凡简,最好定義一個本地的 data 屬性并將這個 prop 用作其初始值:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
-
這個 prop 以一種原始的值傳入且需要進行轉(zhuǎn)換逼友。在這種情況下,最好使用這個 prop 的值來定義一個計算屬性:
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
注意在 JavaScript 中對象和數(shù)組是通過引用傳入的秤涩,所以對于一個數(shù)組或?qū)ο箢愋偷?prop 來說帜乞,在子組件中改變這個對象或數(shù)組本身將會影響到父組件的狀態(tài)。
Prop 驗證
我們可以為組件的 prop 指定驗證要求筐眷,例如你知道的這些類型黎烈。如果有一個需求沒有被滿足,則 Vue 會在瀏覽器控制臺中警告你。這在開發(fā)一個會被別人用到的組件時尤其有幫助照棋。
為了定制 prop 的驗證方式资溃,你可以為 props
中的值提供一個帶有驗證需求的對象,而不是一個字符串?dāng)?shù)組烈炭。例如:
Vue.component('my-component', {
props: {
// 基礎(chǔ)的類型檢查 (`null` 和 `undefined` 會通過任何類型驗證)
propA: Number,
// 多個可能的類型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 帶有默認值的數(shù)字
propD: {
type: Number,
default: 100
},
// 帶有默認值的對象
propE: {
type: Object,
// 對象或數(shù)組默認值必須從一個工廠函數(shù)獲取
default: function () {
return { message: 'hello' }
}
},
// 自定義驗證函數(shù)
propF: {
validator: function (value) {
// 這個值必須匹配下列字符串中的一個
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
當(dāng) prop 驗證失敗的時候溶锭,(開發(fā)環(huán)境構(gòu)建版本的) Vue 將會產(chǎn)生一個控制臺的警告。
注意那些 prop 會在一個組件實例創(chuàng)建之前進行驗證梳庆,所以實例的屬性 (如
data
暖途、computed
等) 在default
或validator
函數(shù)中是不可用的。
類型檢查
type
可以是下列原生構(gòu)造函數(shù)中的一個:
String
Number
Boolean
Array
Object
Date
Function
Symbol
type
還可以是一個自定義的構(gòu)造函數(shù)膏执,并且通過 instanceof
來進行檢查確認驻售。例如,給定下列現(xiàn)成的構(gòu)造函數(shù):
function Person (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
你可以使用:
Vue.component('blog-post', {
props: {
author: Person
}
})
來驗證 author
prop 的值是否是通過 new Person
創(chuàng)建的更米。
非 Prop 的特性
一個非 prop 特性是指傳向一個組件欺栗,但是該組件并沒有相應(yīng) prop 定義的特性。
因為顯式定義的 prop 適用于向一個子組件傳入信息征峦,然而組件庫的作者并不總能預(yù)見組件會被用于怎樣的場景迟几。這也是為什么組件可以接受任意的特性,而這些特性會被添加到這個組件的根元素上栏笆。
例如类腮,想象一下你通過一個 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
厂抽,這是在組件的模板內(nèi)設(shè)置好的 -
date-picker-theme-dark
,這是從組件的父級傳入的
對于絕大多數(shù)特性來說丁眼,從外部提供給組件的值會替換掉組件內(nèi)部設(shè)置好的值筷凤。所以如果傳入 type="text"
就會替換掉 type="date"
并把它破壞!慶幸的是苞七,class
和 style
特性會稍微智能一些藐守,即兩邊的值會被合并起來,從而得到最終的值:form-control date-picker-theme-dark
蹂风。
禁用特性繼承
如果你不希望組件的根元素繼承特性吗伤,你可以在組件的選項中設(shè)置 inheritAttrs: false
。例如:
Vue.component('my-component', {
inheritAttrs: false,
// ...
})
這尤其適合配合實例的 $attrs
屬性使用硫眨,該屬性包含了傳遞給一個組件的特性名和特性值足淆,例如:
{
required: true,
placeholder: 'Enter your username'
}
有了 inheritAttrs: false
和 $attrs
巢块,你就可以手動決定這些特性會被賦予哪個元素。在撰寫[基礎(chǔ)組件]的時候是常會用到的:
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
選項不會影響 style
和 class
的綁定巧号。
這個模式允許你在使用基礎(chǔ)組件的時候更像是使用原始的 HTML 元素族奢,而不會擔(dān)心哪個元素是真正的根元素:
<base-input
v-model="username"
required
placeholder="Enter your username"
></base-input>
自定義事件
事件名
不同于組件和 prop,事件名不存在任何自動化的大小寫轉(zhuǎn)換丹鸿。而是觸發(fā)的事件名需要完全匹配監(jiān)聽這個事件所用的名稱越走。舉個例子,如果觸發(fā)一個 camelCase 名字的事件:
this.$emit('myEvent')
則監(jiān)聽這個名字的 kebab-case 版本是不會有任何效果的:
<!-- 沒有效果 -->
<my-component v-on:my-event="doSomething"></my-component>
不同于組件和 prop靠欢,事件名不會被用作一個 JavaScript 變量名或?qū)傩悦鹊校跃蜎]有理由使用 camelCase 或 PascalCase 了。并且 v-on
事件監(jiān)聽器在 DOM 模板中會被自動轉(zhuǎn)換為全小寫 (因為 HTML 是大小寫不敏感的)门怪,所以 v-on:myEvent
將會變成 v-on:myevent
——導(dǎo)致 myEvent
不可能被監(jiān)聽到骡澈。
推薦始終使用 kebab-case 的事件名。
自定義組件的 v-model
一個組件上的 v-model
默認會利用名為 value
的 prop 和名為 input
的事件掷空,但是像單選框肋殴、復(fù)選框等類型的輸入控件可能會將 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)"
>
`
})
現(xiàn)在在這個組件上使用 v-model
的時候:
<base-checkbox v-model="lovingVue"></base-checkbox>
這里的 lovingVue
的值將會傳入這個名為 checked
的 prop坦弟。同時當(dāng) <base-checkbox>
觸發(fā)一個 change
事件并附帶一個新的值的時候护锤,這個 lovingVue
的屬性將會被更新。
注意你仍然需要在組件的 props
選項里聲明 checked
這個 prop酿傍。
將原生事件綁定到組件
在一個組件的根元素上直接監(jiān)聽一個原生事件烙懦。可以使用 v-on
的 .native
修飾符:
<base-input v-on:focus.native="onFocus"></base-input>
在有的時候這是很有用的赤炒,不過在你嘗試監(jiān)聽一個類似 <input>
的非常特定的元素時氯析,這并不是個好主意。比如上述 <base-input>
組件可能做了如下重構(gòu)可霎,所以根元素實際上是一個 <label>
元素:
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
</label>
這時,父級的 .native
監(jiān)聽器將靜默失敗宴杀。它不會產(chǎn)生任何報錯癣朗,但是 onFocus
處理函數(shù)不會如你預(yù)期地被調(diào)用。
為了解決這個問題旺罢,Vue 提供了一個 $listeners
屬性旷余,它是一個對象,里面包含了作用在這個組件上的所有監(jiān)聽器扁达。例如:
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}
有了這個 $listeners
屬性正卧,你就可以配合 v-on="$listeners"
將所有的事件監(jiān)聽器指向這個組件的某個特定的子元素。對于類似 <input>
的你希望它也可以配合 v-model
工作的組件來說跪解,為這些監(jiān)聽器創(chuàng)建一個類似下述 inputListeners
的計算屬性通常是非常有用的:
Vue.component('base-input', {
inheritAttrs: false,
props: ['label', 'value'],
computed: {
inputListeners: function () {
var vm = this
// `Object.assign` 將所有的對象合并為一個新對象
return Object.assign({},
// 我們從父級添加所有的監(jiān)聽器
this.$listeners,
// 然后我們添加自定義監(jiān)聽器炉旷,
// 或覆寫一些監(jiān)聽器的行為
{
// 這里確保組件配合 `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>
`
})
現(xiàn)在 <base-input>
組件是一個完全透明的包裹器了,也就是說它可以完全像一個普通的 <input>
元素一樣使用了:所有跟它相同的特性和監(jiān)聽器的都可以工作。
.sync
修飾符
在有些情況下窘行,我們可能需要對一個 prop 進行“雙向綁定”饥追。不幸的是,真正的雙向綁定會帶來維護上的問題罐盔,因為子組件可以修改父組件但绕,且在父組件和子組件都沒有明顯的改動來源。
這也是為什么我們推薦以 update:myPropName
的模式觸發(fā)事件取而代之惶看。舉個例子捏顺,在一個包含 title
prop 的假設(shè)的組件中,我們可以用以下方法表達對其賦新值的意圖:
this.$emit('update:title', newTitle)
然后父組件可以監(jiān)聽那個事件并根據(jù)需要更新一個本地的數(shù)據(jù)屬性纬黎。例如:
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
為了方便起見幅骄,我們?yōu)檫@種模式提供一個縮寫,即 .sync
修飾符:
<text-document v-bind:title.sync="doc.title"></text-document>
注意帶有 .sync
修飾符的 v-bind
不能和表達式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’”
是無效的)莹桅。取而代之的是昌执,你只能提供你想要綁定的屬性名,類似 v-model
诈泼。
當(dāng)我們用一個對象同時設(shè)置多個 prop 的時候懂拾,也可以將這個 .sync
修飾符和 v-bind
配合使用:
<text-document v-bind.sync="doc"></text-document>
這樣會把 doc
對象中的每一個屬性 (如 title
) 都作為一個獨立的 prop 傳進去,然后各自添加用于更新的 v-on
監(jiān)聽器铐达。
將 v-bind.sync
用在一個字面量的對象上岖赋,例如 v-bind.sync=”{ title: doc.title }”
,是無法正常工作的瓮孙,因為在解析一個像這樣的復(fù)雜表達式的時候唐断,有很多邊緣情況需要考慮。
插槽
在 2.6.0 中杭抠,我們?yōu)榫呙宀酆妥饔糜虿宀垡肓艘粋€新的統(tǒng)一的語法 (即
v-slot
指令)脸甘。
插槽內(nèi)容
Vue 實現(xiàn)了一套內(nèi)容分發(fā)的 API,將 <slot>
元素作為承載分發(fā)內(nèi)容的出口偏灿。
它允許你像這樣合成組件:
<navigation-link url="/profile">
Your Profile
</navigation-link>
然后你在 <navigation-link>
的模板中可能會寫為:
<a
v-bind:href="url"
class="nav-link"
>
<slot></slot>
</a>
當(dāng)組件渲染的時候丹诀,<slot></slot>
將會被替換為“Your Profile”。插槽內(nèi)可以包含任何模板代碼翁垂,包括 HTML:
<navigation-link url="/profile">
<!-- 添加一個 Font Awesome 圖標(biāo) -->
<span class="fa fa-user"></span>
Your Profile
</navigation-link>
甚至其它的組件:
<navigation-link url="/profile">
<!-- 添加一個圖標(biāo)的組件 -->
<font-awesome-icon name="user"></font-awesome-icon>
Your Profile
</navigation-link>
如果 <navigation-link>
沒有包含一個 <slot>
元素铆遭,則該組件起始標(biāo)簽和結(jié)束標(biāo)簽之間的任何內(nèi)容都會被拋棄。
編譯作用域
當(dāng)你想在一個插槽中使用數(shù)據(jù)時沿猜,例如:
<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> 組件*內(nèi)部*定義的橄妆。
-->
</navigation-link>
作為一條規(guī)則衙伶,請記住:
父級模板里的所有內(nèi)容都是在父級作用域中編譯的呼畸;子模板里的所有內(nèi)容都是在子作用域中編譯的痕支。
后備內(nèi)容(默認的內(nèi)容)
有時為一個插槽設(shè)置具體的后備 (也就是默認的) 內(nèi)容是很有用的,它只會在沒有提供內(nèi)容的時候被渲染蛮原。例如在一個 <submit-button>
組件中:
<button type="submit">
<slot></slot>
</button>
我們可能希望這個 <button>
內(nèi)絕大多數(shù)情況下都渲染文本“Submit”卧须。為了將“Submit”作為后備內(nèi)容,我們可以將它放在 <slot>
標(biāo)簽內(nèi):
<button type="submit">
<slot>Submit</slot>
</button>
現(xiàn)在當(dāng)我在一個父級組件中使用 <submit-button>
并且不提供任何插槽內(nèi)容時:
<submit-button></submit-button>
后備內(nèi)容“Submit”將會被渲染:
<button type="submit">
Submit
</button>
但是如果我們提供內(nèi)容:
<submit-button>
Save
</submit-button>
則這個提供的內(nèi)容將會被渲染從而取代后備內(nèi)容:
<button type="submit">
Save
</button>
具名插槽
有時我們需要多個插槽儒陨。例如對于一個帶有如下模板的 <base-layout>
組件:
<div class="container">
<header>
<!-- 我們希望把頁頭放這里 -->
</header>
<main>
<!-- 我們希望把主要內(nèi)容放這里 -->
</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”蹦漠。
在向具名插槽提供內(nèi)容的時候椭员,我們可以在一個 <template>
元素上使用 v-slot
指令,并以 v-slot
的參數(shù)的形式提供其名稱:
<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>
現(xiàn)在 <template>
元素中的所有內(nèi)容都將會被傳入相應(yīng)的插槽笛园。任何沒有被包裹在帶有 v-slot
的 <template>
中的內(nèi)容都會被視為默認插槽的內(nèi)容隘击。
然而,如果你希望更明確一些研铆,仍然可以在一個 <template>
中包裹默認插槽的內(nèi)容:
<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>
上
作用域插槽
讓插槽內(nèi)容能夠訪問子組件中才有的數(shù)據(jù)是很有用的埋同。例如,設(shè)想一個帶有如下模板的
<current-user>
組件:
<span>
<slot>{{ user.lastName }}</slot>
</span>
我們想讓它的后備內(nèi)容顯示用戶的名棵红,以取代正常情況下用戶的姓凶赁,如下:
<current-user>
{{ user.firstName }}
</current-user>
然而上述代碼不會正常工作,因為只有 <current-user>
組件可以訪問到 user
而我們提供的內(nèi)容是在父級渲染的逆甜。
為了讓 user
在父級的插槽內(nèi)容可用虱肄,我們可以將 user
作為 <slot>
元素的一個特性綁定上去:
<span>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</span>
綁定在 <slot>
元素上的特性被稱為插槽 prop。現(xiàn)在在父級作用域中交煞,我們可以給 v-slot
帶一個值來定義我們提供的插槽 prop 的名字:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
在這個例子中咏窿,我們選擇將包含所有插槽 prop 的對象命名為 slotProps
,但你也可以使用任意你喜歡的名字素征。
獨占默認插槽的縮寫語法
在上述情況下集嵌,當(dāng)被提供的內(nèi)容只有默認插槽時,組件的標(biāo)簽才可以被當(dāng)作插槽的模板來使用稚茅。這樣我們就可以把 v-slot
直接用在組件上:
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>
這種寫法還可以更簡單纸淮。就像假定未指明的內(nèi)容對應(yīng)默認插槽一樣平斩,不帶參數(shù)的 v-slot
被假定對應(yīng)默認插槽:
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
注意默認插槽的縮寫語法不能和具名插槽混用亚享,因為它會導(dǎo)致作用域不明確:
<!-- 無效,會導(dǎo)致警告 -->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</current-user>
只要出現(xiàn)多個插槽绘面,請始終為所有的插槽使用完整的基于 <template>
的語法:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</current-user>
解構(gòu)插槽 Prop
作用域插槽的內(nèi)部工作原理是將你的插槽內(nèi)容包括在一個傳入單個參數(shù)的函數(shù)里:
function (slotProps) {
// 插槽內(nèi)容
}
這意味著 v-slot
的值實際上可以是任何能夠作為函數(shù)定義中的參數(shù)的 JavaScript 表達式欺税。所以在支持的環(huán)境下 (單文件組件或現(xiàn)代瀏覽器)侈沪,你也可以使用 ES2015 解構(gòu)來傳入具體的插槽 prop,如下:
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
這樣可以使模板更簡潔晚凿,尤其是在該插槽提供了多個 prop 的時候亭罪。它同樣開啟了 prop 重命名等其它可能,例如將 user
重命名為 person
:
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
你甚至可以定義后備內(nèi)容歼秽,用于插槽 prop 是 undefined 的情形:
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
動態(tài)插槽名
動態(tài)指令參數(shù)也可以用在 v-slot
上应役,來定義動態(tài)的插槽名:
<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>
然而,和其它指令一樣燥筷,該縮寫只在其有參數(shù)的時候才可用箩祥。這意味著以下語法是無效的:
<!-- 這樣會觸發(fā)一個警告 -->
<current-user #="{ user }">
{{ user.firstName }}
</current-user>
使用縮寫,必須始終以明確插槽名取而代之:
<current-user #default="{ user }">
{{ user.firstName }}
</current-user>
其它示例
插槽 prop 允許我們將插槽轉(zhuǎn)換為可復(fù)用的模板肆氓,這些模板可以基于輸入的 prop 渲染出不同的內(nèi)容袍祖。這在設(shè)計封裝數(shù)據(jù)邏輯同時允許父級組件自定義部分布局的可復(fù)用組件時是最有用的。
例如谢揪,我們要實現(xiàn)一個 <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"
>
<!--
我們?yōu)槊總€ todo 準(zhǔn)備了一個插槽凳鬓,
將 `todo` 對象作為一個插槽的 prop 傳入。
-->
<slot name="todo" v-bind:todo="todo">
<!-- 后備內(nèi)容 -->
{{ todo.text }}
</slot>
</li>
</ul>
現(xiàn)在當(dāng)我們使用 <todo-list>
組件的時候屈雄,我們可以選擇為 todo 定義一個不一樣的 <template>
作為替代方案村视,并且可以從子組件獲取數(shù)據(jù):
<todo-list v-bind:todos="todos">
<template v-slot:todo="{ todo }">
<span v-if="todo.isComplete">?</span>
{{ todo.text }}
</template>
</todo-list>
動態(tài)組件 & 異步組件
在動態(tài)組件上使用 keep-alive
我們之前曾經(jīng)在一個多標(biāo)簽的界面中使用 is
特性來切換不同的組件:
<component v-bind:is="currentTabComponent"></component>
當(dāng)在這些組件之間切換的時候,你有時會想保持這些組件的狀態(tài)酒奶,以避免反復(fù)重渲染導(dǎo)致的性能問題蚁孔。例如我們來展開說一說這個多標(biāo)簽界面:
Posts Archive
- Cat Ipsum
- Hipster Ipsum
- Cupcake Ipsum
Click on a blog title to the left to view it.
你會注意到,如果你選擇了一篇文章惋嚎,切換到 Archive 標(biāo)簽招狸,然后再切換回 Posts,是不會繼續(xù)展示你之前選擇的文章的浪册。這是因為你每次切換新標(biāo)簽的時候团搞,Vue 都創(chuàng)建了一個新的 currentTabComponent
實例。
重新創(chuàng)建動態(tài)組件的行為通常是非常有用的摆尝,但是在這個案例中温艇,我們更希望那些標(biāo)簽的組件實例能夠被在它們第一次被創(chuàng)建的時候緩存下來。為了解決這個問題堕汞,我們可以用一個 <keep-alive>
元素將其動態(tài)組件包裹起來勺爱。
<!-- 失活的組件將會被緩存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
注意這個 <keep-alive>
要求被切換到的組件都有自己的名字讯检,不論是通過組件的 name
選項還是局部/全局注冊琐鲁。
異步組件
在大型應(yīng)用中卫旱,我們可能需要將應(yīng)用分割成小一些的代碼塊,并且只在需要的時候才從服務(wù)器加載一個模塊围段。為了簡化顾翼,Vue 允許你以一個工廠函數(shù)的方式定義你的組件,這個工廠函數(shù)會異步解析你的組件定義奈泪。Vue 只有在這個組件需要被渲染的時候才會觸發(fā)該工廠函數(shù)适贸,且會把結(jié)果緩存起來供未來重渲染。例如:
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回調(diào)傳遞組件定義
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
如你所見涝桅,這個工廠函數(shù)會收到一個 resolve
回調(diào)取逾,這個回調(diào)函數(shù)會在你從服務(wù)器得到組件定義的時候被調(diào)用。你也可以調(diào)用 reject(reason)
來表示加載失敗苹支。這里的 setTimeout
是為了演示用的砾隅,如何獲取組件取決于你自己。一個推薦的做法是將異步組件和 webpack 的 code-splitting 功能一起配合使用:
Vue.component('async-webpack-example', function (resolve) {
// 這個特殊的 `require` 語法將會告訴 webpack
// 自動將你的構(gòu)建代碼切割成多個包债蜜,這些包
// 會通過 Ajax 請求加載
require(['./my-async-component'], resolve)
})
你也可以在工廠函數(shù)中返回一個 Promise
晴埂,所以把 webpack 2 和 ES2015 語法加在一起,我們可以寫成這樣:
Vue.component(
'async-webpack-example',
// 這個 `import` 函數(shù)會返回一個 `Promise` 對象寻定。
() => import('./my-async-component')
)
當(dāng)使用局部注冊的時候儒洛,你也可以直接提供一個返回 Promise
的函數(shù):
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
處理加載狀態(tài)
這里的異步組件工廠函數(shù)也可以返回一個如下格式的對象:
const AsyncComponent = () => ({
// 需要加載的組件 (應(yīng)該是一個 `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 來訪問或使用处硬。
// 獲取根組件的數(shù)據(jù)
this.$root.foo
// 寫入根組件的數(shù)據(jù)
this.$root.foo = 2
// 訪問根組件的計算屬性
this.$root.bar
// 調(diào)用根組件的方法
this.$root.baz()
對于 demo 或非常小型的有少量組件的應(yīng)用來說這是很方便的。不過這個模式擴展到中大型應(yīng)用來說就不然了拇派。因此在絕大多數(shù)情況下荷辕,我們強烈推薦使用 Vuex 來管理應(yīng)用的狀態(tài)。
訪問父級組件實例
和
$root
類似件豌,$parent
屬性可以用來從一個子組件訪問父組件的實例疮方。它提供了一種機會,可以在后期隨時觸達父級組件茧彤,以替代將數(shù)據(jù)以 prop 的方式傳入子組件的方式骡显。
在絕大多數(shù)情況下,觸達父級組件會使得你的應(yīng)用更難調(diào)試和理解,尤其是當(dāng)你變更了父級組件的數(shù)據(jù)的時候蟆盐。當(dāng)我們稍后回看那個組件的時候,很難找出那個變更是從哪里發(fā)起的遭殉。
另外在一些可能適當(dāng)?shù)臅r候石挂,你需要特別地共享一些組件庫。舉個例子险污,在和 JavaScript API 進行交互而不渲染 HTML 的抽象組件內(nèi)痹愚,諸如這些假設(shè)性的 Google 地圖組件一樣:
<google-map>
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>
這個 <google-map>
組件可以定義一個 map
屬性,所有的子組件都需要訪問它蛔糯。在這種情況下 <google-map-markers>
可能想要通過類似 this.$parent.getMap
的方式訪問那個地圖拯腮,以便為其添加一組標(biāo)記。你可以在這里查閱這種模式蚁飒。
請留意动壤,盡管如此,通過這種模式構(gòu)建出來的那個組件的內(nèi)部仍然是容易出現(xiàn)問題的淮逻。比如琼懊,設(shè)想一下我們添加一個新的 <google-map-region>
組件,當(dāng) <google-map-markers>
在其內(nèi)部出現(xiàn)的時候爬早,只會渲染那個區(qū)域內(nèi)的標(biāo)記:
<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>
內(nèi)部你可能發(fā)現(xiàn)自己需要一些類似這樣的 hack:
var map = this.$parent.map || this.$parent.$parent.map
很快它就會失控哼丈。這也是我們針對需要向任意更深層級的組件提供上下文信息時推薦依賴注入的原因。
訪問子組件實例或子元素
盡管存在 prop 和事件筛严,有的時候你仍可能需要在 JavaScript 里直接訪問一個子組件醉旦。為了達到這個目的,你可以通過
ref
特性為這個子組件賦予一個 ID 引用桨啃。例如:
<base-input ref="usernameInput"></base-input>
現(xiàn)在在你已經(jīng)定義了這個 ref
的組件里车胡,你可以使用:
this.$refs.usernameInput
來訪問這個 <base-input>
實例,以便不時之需照瘾。比如程序化地從一個父級組件聚焦這個輸入框吨拍。在剛才那個例子中,該 <base-input>
組件也可以使用一個類似的 ref
提供對內(nèi)部這個指定元素的訪問网杆,例如:
<input ref="input">
甚至可以通過其父級組件定義方法:
methods: {
// 用來從父級組件聚焦輸入框
focus: function () {
this.$refs.input.focus()
}
}
這樣就允許父級組件通過下面的代碼聚焦 <base-input>
里的輸入框:
this.$refs.usernameInput.focus()
當(dāng) ref
和 v-for
一起使用的時候羹饰,你得到的引用將會是一個包含了對應(yīng)數(shù)據(jù)源的這些子組件的數(shù)組。
$refs
只會在組件渲染完成之后生效碳却,并且它們不是響應(yīng)式的队秩。這僅作為一個用于直接操作子組件的“逃生艙”——你應(yīng)該避免在模板或計算屬性中訪問 $refs
。
程序化的事件偵聽器
現(xiàn)在昼浦,你已經(jīng)知道了 $emit
的用法馍资,它可以被 v-on
偵聽,但是 Vue 實例同時在其事件接口中提供了其它的方法关噪。我們可以:
- 通過
$on(eventName, eventHandler)
偵聽一個事件 - 通過
$once(eventName, eventHandler)
一次性偵聽一個事件 - 通過
$off(eventName, eventHandler)
停止偵聽一個事件
你通常不會用到這些鸟蟹,但是當(dāng)你需要在一個組件實例上手動偵聽事件時乌妙,它們是派得上用場的。它們也可以用于代碼組織工具建钥。例如藤韵,你可能經(jīng)常看到這種集成一個第三方庫的模式:
// 一次性將這個日期選擇器附加到一個輸入框上
// 它會被掛載到 DOM 上熊经。
mounted: function () {
// Pikaday 是一個第三方日期選擇器的庫
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// 在組件被銷毀之前泽艘,
// 也銷毀這個日期選擇器。
beforeDestroy: function () {
this.picker.destroy()
}
這里有兩個潛在的問題:
- 它需要在這個組件實例中保存這個
picker
镐依,如果可以的話最好只有生命周期鉤子可以訪問到它匹涮。這并不算嚴重的問題,但是它可以被視為雜物槐壳。 - 我們的建立代碼獨立于我們的清理代碼然低,這使得我們比較難于程序化地清理我們建立的所有東西。
你應(yīng)該通過一個程序化的偵聽器解決這兩個問題:
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 的事件系統(tǒng)不同于瀏覽器的 EventTarget API。盡管它們工作起來是相似的绍哎,但是 $emit
来农、$on
, 和 $off
并不是 dispatchEvent
、addEventListener
和 removeEventListener
的別名崇堰。