處理邊界情況
該頁面假設你已經閱讀過了元件基礎。如果你還對組件不太了解,推薦你先閱讀它。
這裡記錄的都是和處理邊界狀況有關的功能,也就是一些需要對 Vue 的規則做一些小調整的特殊情況。不過注意這些功能都是有劣勢或危險的場景的。我們會在每個案例中註明,所以當你使用每個功能的時候請稍加留意。
目錄
- 程式化的事件偵聽器
- 控制更新
存取元素& 元件
#在絕大多數情況下,我們最好不要觸達另一個元件實例內部或手動操作DOM 元素。不過也確實在某些情況下做這些事情是適當的。
// 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
#$root
類似,
$parent 屬性可以用來從一個子元件存取父元件的實例。它提供了一種機會,可以在後期隨時觸及父級元件,以替代將資料以 prop 的方式傳入子元件的方式。 在絕大多數情況下,觸達父級元件會讓你的應用程式更難除錯和理解,尤其是當你變更了父級元件的資料的時候。當我們稍後回看那個組件的時候,很難找出那個變更是從哪裡發起的。
另外在一些可能適當的時候,你需要特別地共用一些元件庫。舉個例子,在和JavaScript API 進行互動而不渲染HTML 的抽像元件內,諸如這些假設性的Google 地圖元件一樣:
這個<google-map> 元件可以定義一個<google-map>
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>
屬性,所有的子元件都需要存取它。在這種情況下 <google-map-markers>
可能想要透過類似 this.$parent.getMap
的方式存取該地圖,以便為其添加一組標記。你可以在
查閱這種模式。 請留意,儘管如此,透過這種模式建構出來的那個元件的內部仍然是容易出現問題的。例如,設想一下我們增加一個新的
元件,當<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>
var map = this.$parent.map || this.$parent.$parent.map很快它就會失控。這也是我們針對需要向任意更深層的元件提供上下文資訊時推薦依賴注入的原因。
訪問子元件實例或子元素
<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()
當ref
和v-for
一起使用的時候,你得到的引用將會是一個包含了對應資料來源的這些子元件的陣列。
$refs
只會在元件渲染完成之後生效,而且它們不是響應式的。這僅作為一個用於直接操作子元件的「逃生艙」——你應該避免在模板或計算屬性中存取$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
方法,以便知道要跟哪個地圖互動。不幸的是,使用 $parent
屬性無法很好的擴展到更深層的巢狀元件上。這也是依賴注入的用武之地,它用到了兩個新的實例選項:provide
和 inject
。
provide
選項允許我們指定我們想要提供給後代元件的資料/方法。在這個例子中,就是<google-map>
內部的getMap
方法:
provide: function () { return { getMap: this.getMap } }
然後在任何後代元件裡,我們都可以使用 inject
選項來接收指定的我們想要添加在這個實例上的屬性:
inject: ['getMap']
你可以在這裡看到完整的範例。相較之下$parent
來說,這個用法可以讓我們在任意後代元件中存取getMap
,而不需要暴露整個<google-map>
實例。這允許我們更好的持續研發該組件,而不需要擔心我們可能會改變/移除一些子組件依賴的東西。同時這些元件之間的介面是始終明確定義的,就和 props
一樣。
實際上,你可以把依賴注入看作一部分“大範圍有效的prop”,除了:
祖先元件不需要知道哪些後代元件使用它提供的屬性
後位元件不需要知道被注入的屬性來自哪裡
然而,依賴注入還是有負面影響的。它將你應用程式中的元件與它們目前的組織方式耦合起來,使重構變得更加困難。同時所提供的屬性是非響應式的。這是出於設計的考慮,因為使用它們來創建一個中心化規模化的資料跟使用
$root
做這件事都是不夠好的。如果你想要共享的這個屬性是你的應用程式特有的,而不是通用化的,或者如果你想在祖先元件中更新所提供的數據,那麼這意味著你可能需要換用一個像 Vuex 這樣真正的狀態管理方案了。
你可以在 API 參考文件學習更多關於依賴注入的知識。
程式化的事件偵聽器
現在,你已經知道了$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() }) } }查閱
這個fiddle 可以了解完整的程式碼。注意,即便如此,如果你發現自己必須在單一元件裡做很多建立和清理的工作,最好的方式通常還是創建更多的模組化元件。在這個範例中,我們推薦建立一個可重複使用的 <input-datepicker> 元件。
想了解更多程式化偵聽器的內容,請查閱實例方法 / 事件相關的 API。
注意 Vue 的事件系統有別於瀏覽器的 EventTarget API。雖然它們運作起來是相似的,但是
$emit
、$on
, 和$off
並不是dispatchEvent
#、addEventListener
和removeEventListener
的別名。
#循環參考
遞迴元件
元件是可以在它們自己的模板中呼叫自身的。不過它們只能透過name
選項來做這件事:
name: 'unique-name-of-my-component'
當你使用Vue.component
全域註冊一個元件時,這個全域的ID 會自動設定為該元件的name
選項。
Vue.component('unique-name-of-my-component', { // ... })
稍有不慎,遞歸元件就可能導致無限迴圈:
name: 'stack-overflow', template: '<div><stack-overflow></stack-overflow></div>'
類似上述的元件將會導致「max stack size exceeded」錯誤,所以請確保遞歸呼叫是條件性的(例如使用一個最終會得到false
的v-if
)。
元件之間的循環參考
#假設你需要建立一個檔案目錄樹,像訪達或資源管理器那樣的。你可能有一個<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>
當你仔細觀察的時候,你會發現這些元件在渲染樹中互為對方的後代和祖先——一個悖論!當透過 Vue.component
全域註冊元件的時候,這個悖論會被自動解開。如果你是這樣做的,那麼你可以跳過這裡。
然而,如果你使用一個模組系統依賴/導入元件,例如透過webpack 或Browserify,你會遇到一個錯誤:
Failed to mount component: template or render function not defined.
為了解釋這裡發生了什麼,我們先把兩個組件稱為A 和B。模組系統發現它需要 A,但首先 A 依賴 B,但 B 又依賴 A,但 A 又依賴 B,如此往復。這變成了一個循環,不知道如何不經過其中一個元件而完全解析出另一個元件。為了解決這個問題,我們需要給模組系統一個點,在那裡「A 反正是需要 B 的,但我們不需要先解析 B。」
在我們的例子中,把 <tree-folder>
元件設為了那個點。我們知道那個產生悖論的子元件是<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') }
這樣問題就解決了!
模板定義的替代品
內嵌模板
當inline-template
這個特殊的特性出現在一個子元件上時,這個元件將會使用裡面的內容作為模板,而不是將其作為被分發的內容。這使得模板的撰寫工作更加靈活。
<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>
內嵌範本需要定義在 Vue 所屬的 DOM 元素內。
不過,
inline-template
會讓模板的作用域變得更難以理解。所以作為最佳實踐,請在元件內優先選擇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 或極小的應用,但是其它情況下請避免使用,因為這會將模板和該組件的其它定義分離開。
控制更新
#感謝Vue 的響應式系統,它始終知道何時進行更新(如果你用對了的話)。不過還是有一些邊界情況,你想要強制更新,儘管表面上看響應式的資料沒有改變。也有一些情況是你想阻止不必要的更新。
強制更新
#如果你發現你自己需要在 Vue 中做一次強制更新,99.9% 的情況,是你在某個地方做錯了事。
你可能還沒留意到陣列或物件的變更偵測注意事項,或者你可能依賴了一個未被Vue 的響應式系統追踪的狀態。
然而,如果你已經做到了上述的事項仍然發現在極少數的情況下需要手動強制更新,那麼你可以透過 $forceUpdate
來做這件事。
透過v-once
建立低開銷的靜態元件
大量靜態內容。在這種情況下,你可以在根元素上加上v-once 功能以確保這些內容只計算一次然後快取起來,就像這樣:
Vue.component('terms-of-service', { template: ` <div v-once> <h1>Terms of Service</h1> ... a lot of static content ... </div> ` })
再說一次,試著不要過度使用這個模式。當你需要渲染大量靜態內容時,極少數的情況下它會給你帶來便利,除非你非常留意渲染變慢了,不然它完全是沒有必要的——再加上它在後期會帶來很多困惑。例如,設想另一個開發者並不熟悉v-once
或錯過了它在模板中,他們可能會花很多個小時去找出模板為什麼無法正確更新。
#