處理邊界情況
這里記錄的都是和處理邊界情況有關(guān)的功能,即一些需要對 Vue 的規(guī)則做一些小調(diào)整的特殊情況。
該頁面假設(shè)你已經(jīng)閱讀過了組件基礎(chǔ)。如果你還對組件不太了解,推薦你先閱讀它。
這里記錄的都是和處理邊界情況有關(guān)的功能,即一些需要對 Vue 的規(guī)則做一些小調(diào)整的特殊情況。不過注意這些功能都是有劣勢或危險的場景的。我們會在每個案例中注明,所以當(dāng)你使用每個功能的時候請稍加留意。
目錄
訪問元素 & 組件
在絕大多數(shù)情況下,我們最好不要觸達(dá)另一個組件實例內(nèi)部或手動操作 DOM 元素。不過也確實在一些情況下做這些事情是合適的。
訪問根實例
在每個 new Vue
實例的子組件中,其根實例可以通過 $root
屬性進(jìn)行訪問。例如,在這個根實例中:
// 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)用來說這是很方便的。不過這個模式擴(kuò)展到中大型應(yīng)用來說就不然了。因此在絕大多數(shù)情況下,我們強(qiáng)烈推薦使用 Vuex 來管理應(yīng)用的狀態(tài)。
訪問父級組件實例
和 $root
類似,$parent
屬性可以用來從一個子組件訪問父組件的實例。它提供了一種機(jī)會,可以在后期隨時觸達(dá)父級組件,以替代將數(shù)據(jù)以 prop 的方式傳入子組件的方式。
在絕大多數(shù)情況下,觸達(dá)父級組件會使得你的應(yīng)用更難調(diào)試和理解,尤其是當(dāng)你變更了父級組件的數(shù)據(jù)的時候。當(dāng)我們稍后回看那個組件的時候,很難找出那個變更是從哪里發(fā)起的。
另外在一些可能適當(dāng)?shù)臅r候,你需要特別地共享一些組件庫。舉個例子,在和 JavaScript API 進(jìn)行交互而不渲染 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 里直接訪問一個子組件。為了達(dá)到這個目的,你可以通過 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
。
依賴注入
在此之前,在我們描述訪問父級組件實例的時候,展示過一個類似這樣的例子:
<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>
的后代都需要訪問一個 getMap
方法,以便知道要跟哪個地圖進(jìn)行交互。不幸的是,使用 $parent
屬性無法很好的擴(kuò)展到更深層級的嵌套組件上。這也是依賴注入的用武之地,它用到了兩個新的實例選項:provide
和 inject
。
provide
選項允許我們指定我們想要提供給后代組件的數(shù)據(jù)/方法。在這個例子中,就是 <google-map>
內(nèi)部的 getMap
方法:
provide: function () { return { getMap: this.getMap } }
然后在任何后代組件里,我們都可以使用 inject
選項來接收指定的我們想要添加在這個實例上的屬性:
inject: ['getMap']
你可以在這里看到完整的示例。相比 $parent
來說,這個用法可以讓我們在任意后代組件中訪問 getMap
,而不需要暴露整個 <google-map>
實例。這允許我們更好的持續(xù)研發(fā)該組件,而不需要擔(dān)心我們可能會改變/移除一些子組件依賴的東西。同時這些組件之間的接口是始終明確定義的,就和 props
一樣。
實際上,你可以把依賴注入看作一部分“大范圍有效的 prop”,除了:
祖先組件不需要知道哪些后代組件使用它提供的屬性
后代組件不需要知道被注入的屬性來自哪里
然而,依賴注入還是有負(fù)面影響的。它將你應(yīng)用程序中的組件與它們當(dāng)前的組織方式耦合起來,使重構(gòu)變得更加困難。同時所提供的屬性是非響應(yīng)式的。這是出于設(shè)計的考慮,因為使用它們來創(chuàng)建一個中心化規(guī)?;臄?shù)據(jù)跟使用
$root
做這件事都是不夠好的。如果你想要共享的這個屬性是你的應(yīng)用特有的,而不是通用化的,或者如果你想在祖先組件中更新所提供的數(shù)據(jù),那么這意味著你可能需要換用一個像 Vuex 這樣真正的狀態(tài)管理方案了。
你可以在 API 參考文檔學(xué)習(xí)更多關(guān)于依賴注入的知識。
程序化的事件偵聽器
現(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án)重的問題,但是它可以被視為雜物。我們的建立代碼獨(dú)立于我們的清理代碼,這使得我們比較難于程序化地清理我們建立的所有東西。
你應(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() }) } }
查閱這個 fiddle 可以了解到完整的代碼。注意,即便如此,如果你發(fā)現(xiàn)自己不得不在單個組件里做很多建立和清理的工作,最好的方式通常還是創(chuàng)建更多的模塊化組件。在這個例子中,我們推薦創(chuàng)建一個可復(fù)用的 <input-datepicker>
組件。
想了解更多程序化偵聽器的內(nèi)容,請查閱實例方法 / 事件相關(guān)的 API。
注意 Vue 的事件系統(tǒng)不同于瀏覽器的 EventTarget API。盡管它們工作起來是相似的,但是
$emit
、$on
, 和$off
并不是dispatchEvent
、addEventListener
和removeEventListener
的別名。
循環(huán)引用
遞歸組件
組件是可以在它們自己的模板中調(diào)用自身的。不過它們只能通過 name
選項來做這件事:
name: 'unique-name-of-my-component'
當(dāng)你使用 Vue.component
全局注冊一個組件時,這個全局的 ID 會自動設(shè)置為該組件的 name
選項。
Vue.component('unique-name-of-my-component', { // ... })
稍有不慎,遞歸組件就可能導(dǎo)致無限循環(huán):
name: 'stack-overflow', template: '<div><stack-overflow></stack-overflow></div>'
類似上述的組件將會導(dǎo)致“max stack size exceeded”錯誤,所以請確保遞歸調(diào)用是條件性的 (例如使用一個最終會得到 false
的 v-if
)。
組件之間的循環(huán)引用
假設(shè)你需要構(gòu)建一個文件目錄樹,像訪達(dá)或資源管理器那樣的。你可能有一個 <tree-folder>
組件,模板是這樣的:
<p> <span>{{ folder.name }}</span> <tree-folder-contents :children="folder.children"/> </p>
還有一個 <tree-folder-contents>
組件,模板是這樣的:
<ul> <li v-for="child in children"> <tree-folder v-if="child.children" :folder="child"/> <span v-else>{{ child.name }}</span> </li> </ul>
當(dāng)你仔細(xì)觀察的時候,你會發(fā)現(xiàn)這些組件在渲染樹中互為對方的后代和祖先——一個悖論!當(dāng)通過 Vue.component
全局注冊組件的時候,這個悖論會被自動解開。如果你是這樣做的,那么你可以跳過這里。
然而,如果你使用一個模塊系統(tǒng)依賴/導(dǎo)入組件,例如通過 webpack 或 Browserify,你會遇到一個錯誤:
Failed to mount component: template or render function not defined.
為了解釋這里發(fā)生了什么,我們先把兩個組件稱為 A 和 B。模塊系統(tǒng)發(fā)現(xiàn)它需要 A,但是首先 A 依賴 B,但是 B 又依賴 A,但是 A 又依賴 B,如此往復(fù)。這變成了一個循環(huán),不知道如何不經(jīng)過其中一個組件而完全解析出另一個組件。為了解決這個問題,我們需要給模塊系統(tǒng)一個點(diǎn),在那里“A 反正是需要 B 的,但是我們不需要先解析 B?!?/p>
在我們的例子中,把 <tree-folder>
組件設(shè)為了那個點(diǎn)。我們知道那個產(chǎn)生悖論的子組件是 <tree-folder-contents>
組件,所以我們會等到生命周期鉤子 beforeCreate
時去注冊它:
beforeCreate: function () { this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default }
或者,在本地注冊組件的時候,你可以使用 webpack 的異步 import
:
components: { TreeFolderContents: () => import('./tree-folder-contents.vue') }
這樣問題就解決了!
模板定義的替代品
內(nèi)聯(lián)模板
當(dāng) inline-template
這個特殊的特性出現(xiàn)在一個子組件上時,這個組件將會使用其里面的內(nèi)容作為模板,而不是將其作為被分發(fā)的內(nèi)容。這使得模板的撰寫工作更加靈活。
<my-component inline-template> <div> <p>These are compiled as the component's own template.</p> <p>Not parent's transclusion content.</p> </div> </my-component>
內(nèi)聯(lián)模板需要定義在 Vue 所屬的 DOM 元素內(nèi)。
不過,
inline-template
會讓模板的作用域變得更加難以理解。所以作為最佳實踐,請在組件內(nèi)優(yōu)先選擇template
選項或.vue
文件里的一個<template>
元素來定義模板。
X-Template
另一個定義模板的方式是在一個 <script>
元素中,并為其帶上 text/x-template
的類型,然后通過一個 id 將模板引用過去。例如:
<script type="text/x-template" id="hello-world-template"> <p>Hello hello hello</p> </script> Vue.component('hello-world', { template: '#hello-world-template' })
x-template 需要定義在 Vue 所屬的 DOM 元素外。
這些可以用于模板特別大的 demo 或極小型的應(yīng)用,但是其它情況下請避免使用,因為這會將模板和該組件的其它定義分離開。
控制更新
感謝 Vue 的響應(yīng)式系統(tǒng),它始終知道何時進(jìn)行更新 (如果你用對了的話)。不過還是有一些邊界情況,你想要強(qiáng)制更新,盡管表面上看響應(yīng)式的數(shù)據(jù)沒有發(fā)生改變。也有一些情況是你想阻止不必要的更新。
強(qiáng)制更新
如果你發(fā)現(xiàn)你自己需要在 Vue 中做一次強(qiáng)制更新,99.9% 的情況,是你在某個地方做錯了事。
你可能還沒有留意到數(shù)組或對象的變更檢測注意事項,或者你可能依賴了一個未被 Vue 的響應(yīng)式系統(tǒng)追蹤的狀態(tài)。
然而,如果你已經(jīng)做到了上述的事項仍然發(fā)現(xiàn)在極少數(shù)的情況下需要手動強(qiáng)制更新,那么你可以通過 $forceUpdate
來做這件事。
通過 v-once
創(chuàng)建低開銷的靜態(tài)組件
渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的時候你可能有一個組件,這個組件包含了大量靜態(tài)內(nèi)容。在這種情況下,你可以在根元素上添加 v-once
特性以確保這些內(nèi)容只計算一次然后緩存起來,就像這樣:
Vue.component('terms-of-service', { template: ` <div v-once> <h1>Terms of Service</h1> ... a lot of static content ... </div> ` })
再說一次,試著不要過度使用這個模式。當(dāng)你需要渲染大量靜態(tài)內(nèi)容時,極少數(shù)的情況下它會給你帶來便利,除非你非常留意渲染變慢了,不然它完全是沒有必要的——再加上它在后期會帶來很多困惑。例如,設(shè)想另一個開發(fā)者并不熟悉
v-once
或漏看了它在模板中,他們可能會花很多個小時去找出模板為什么無法正確更新。