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