Vue.js 组件 处理边界情况
這里記錄的都是和處理邊界情況有關的功能,即一些需要對 Vue 的規則做一些小調整的特殊情況。不過注意這些功能都是有劣勢或危險的場景的。我們會在每個案例中注明,所以當你使用每個功能的時候請稍加留意。
訪問元素 & 組件
在絕大多數情況下,我們最好不要觸達另一個組件實例內部或手動操作 DOM 元素。不過也確實在一些情況下做這些事情是合適的。
訪問根實例
在每個?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() |
當?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> |
不過,inline-template?會讓你模板的作用域變得更加難以理解。所以作為最佳實踐,請在組件內優先選擇?template?選項或?.vue?文件里的一個?<template>?元素來定義模板。
X-Templates
另一個定義模板的方式是在一個?<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' }) |
這些可以用于模板特別大的 demo 或極小型的應用,但是其它情況下請避免使用,因為這會將模板和該組件的其它定義分離開。
控制更新
感謝 Vue 的響應式系統,它始終知道何時進行更新 (如果你用對了的話)。不過還是有一些邊界情況,你想要強制更新,盡管表面上看響應式的數據沒有發生改變。也有一些情況是你想阻止不必要的更新。
強制更新
如果你發現你自己需要在 Vue 中做一次強制更新,99.9% 的情況,是你在某個地方做錯了事。
你可能還沒有留意到數組或對象的變更檢測注意事項,或者你可能依賴了一個未被 Vue 的響應式系統追蹤的狀態。
然而,如果你已經做到了上述的事項仍然發現在極少數的情況下需要手動強制更新,那么你可以通過?$forceUpdate?來做這件事。
通過?v-once?創建低開銷的靜態組件
渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的時候你可能有一個組件,這個組件包含了大量靜態內容。在這種情況下,你可以在根元素上添加?v-once?特性以確保這些內容只計算一次然后緩存起來,就像這樣:
| Vue.component('terms-of-service', {template: `<div v-once><h1>Terms of Service</h1>... a lot of static content ...</div>` }) |
再說一次,試著不要過度使用這個模式。當你需要渲染大量靜態內容時,極少數的情況下它會給你帶來便利,除非你非常留意渲染變慢了,不然它完全是沒有必要的——再加上它在后期會帶來很多困惑。例如,設想另一個開發者并不熟悉?v-once?或漏看了它在模板中,他們可能會花很多個小時去找出模板為什么無法正確更新。
?
from:?https://cn.vuejs.org/v2/guide/components-edge-cases.html
總結
以上是生活随笔為你收集整理的Vue.js 组件 处理边界情况的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Vue.js 动态组件 异步组件
- 下一篇: Vue.js 进入/离开 列表过渡