Vue學習筆記9-深刻了解組件3-動態組件、異步組件、邊界處理

前言

這塊基本參照官方文檔與API文檔進行知識點整理。vue

動態組件

咱們以前曾經在一個多標籤的界面中使用 is attribute 來切換不一樣的組件(在學習筆記4中)webpack

<component v-bind:is="currentTabComponent"></component>
複製代碼

當在這些組件之間切換的時候,你有時會想保持這些組件的狀態,以免反覆重渲染致使的性能問題。web

每次切換新標籤的時候,Vue 都建立了一個新的 currentTabComponent 實例。致使若是切換選項卡,不會保存以前選項卡中的選擇結果。api

從新建立動態組件的行爲一般是很是有用的,可是在這個案例中,咱們更但願那些標籤的組件實例可以被在它們第一次被建立的時候緩存下來。爲了解決這個問題,咱們能夠用一個 <keep-alive> 元素將其動態組件包裹起來。數組

<!-- 失活的組件將會被緩存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>
複製代碼

如今這個 Posts 標籤保持了它的狀態 (被選中的文章) 甚至當它未被渲染時也是如此。緩存

注意這個<keep-alive>要求被切換到的組件都有本身的名字,不管是經過組件的 name 選項仍是局部/全局註冊。它自身不會渲染一個 DOM 元素,也不會出如今組件的父組件鏈中。bash

主要用於保留組件狀態或避免從新渲染。服務器

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

<!-- 多個條件判斷的子組件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

<!-- 和 `<transition>` 一塊兒使用 -->
<transition>
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>
複製代碼

注意,<keep-alive> 是用在其一個直屬的子組件被開關的情形。若是你在其中有 v-for 則不會工做。若是有上述的多個條件性的子元素,<keep-alive> 要求同時只有一個子元素被渲染。app

異步組件

在大型應用中,咱們可能須要將應用分割成小一些的代碼塊,而且只在須要的時候才從服務器加載一個模塊。(公司實習中這樣的問題更爲常見了)異步

Vue 容許你以一個工廠函數的方式定義你的組件,這個工廠函數會異步解析你的組件定義。Vue 只有在這個組件須要被渲染的時候纔會觸發該工廠函數,且會把結果緩存起來供將來重渲染。

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回調傳遞組件定義
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})
複製代碼

如你所見,這個工廠函數會收到一個 resolve 回調,這個回調函數會在你從服務器獲得組件定義的時候被調用。你也能夠調用 reject(reason) 來表示加載失敗。這裏的 setTimeout 是爲了演示用的,如何獲取組件取決於你本身。一個推薦的作法是將異步組件和 webpack 的 code-splitting 功能一塊兒配合使用。

Vue.component('async-webpack-example', function (resolve) {
  // 這個特殊的 `require` 語法將會告訴 webpack
  // 自動將你的構建代碼切割成多個包,這些包
  // 會經過 Ajax 請求加載
  require(['./my-async-component'], resolve)
})
複製代碼

你也能夠在工廠函數中返回一個 Promise,因此把 webpack 2 和 ES2015 語法加在一塊兒,咱們能夠這樣使用動態導入

Vue.component(
  'async-webpack-example',
  // 這個動態導入會返回一個 `Promise` 對象。
  () => import('./my-async-component')
)

當使用局部註冊的時候,你也能夠直接提供一個返回 Promise 的函數

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})
複製代碼

處理加載狀態

異步組件工廠函數也能夠返回一個以下格式的對象

const AsyncComponent = () => ({
  // 須要加載的組件 (應該是一個 `Promise` 對象)
  component: import('./MyComponent.vue'),
  // 異步組件加載時使用的組件
  loading: LoadingComponent,
  // 加載失敗時使用的組件
  error: ErrorComponent,
  // 展現加載時組件的延時時間。默認值是 200 (毫秒)
  delay: 200,
  // 若是提供了超時時間且組件加載也超時了,
  // 則使用加載失敗時使用的組件。默認值是:`Infinity`
  timeout: 3000
})
複製代碼

處理邊界狀況

訪問元素與組件

在絕大多數狀況下,咱們最好不要觸達另外一個組件實例內部或手動操做 DOM 元素

訪問根實例

在每一個 new Vue 實例的子組件中,其根實例能夠經過 $root property 進行訪問。

// 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()
複製代碼

這個模式擴展到中大型應用來講就否則了。所以在絕大多數狀況下,咱們強烈推薦使用 Vuex 來管理應用的狀態。

訪問父級組件實例

$root 相似,$parent property 能夠用來從一個子組件訪問父組件的實例。它提供了一種機會,能夠在後期隨時觸達父級組件,以替代將數據以 prop 的方式傳入子組件的方式。

在絕大多數狀況下,觸達父級組件會使得你的應用更難調試和理解,尤爲是當你變動了父級組件的數據的時候。當咱們稍後回看那個組件的時候,很難找出那個變動是從哪裏發起的。慎用!慎用!慎用!

在一些可能適當的時候,你須要特別地共享一些組件庫。舉個例子,在和 JavaScript API 進行交互而不渲染 HTML 的抽象組件內,諸如這些假設性的 Google 地圖組件同樣

<google-map>
  <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>
複製代碼

這個 <google-map> 組件能夠定義一個 map property,全部的子組件都須要訪問它。在這種狀況下 <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
複製代碼

很快它就會失控。咱們針對須要向任意更深層級的組件提供上下文信息時推薦依賴注入

依賴注入

使用 $parent property 沒法很好的擴展到更深層級的嵌套組件上。這也是依賴注入的用武之地,它用到了兩個新的實例選項:provide 和 inject。使用它們子組件就可使用父組件的數據和方法辣!

  • provide 選項容許咱們指定咱們想要提供給後代組件的數據/方法

在這個例子中,就是 內部的 getMap 方法

provide: function () {
  return {
    getMap: this.getMap
  }
}
複製代碼
  • 而後在任何後代組件裏,咱們均可以使用 inject 選項來接收指定的咱們想要添加在這個實例上的 property
inject: ['getMap']
複製代碼

完整demo代碼:

<div id="app">
      <google-map>
        <google-map-marker v-bind:places="vueConfCities"></google-map-marker>
        <!-->須要訪問父級的map property<-->
      </google-map>
    </div>

    <script>
      Vue.component("google-map", {
        provide: function() {
          return {
            getMap: this.getMap//咱們指定咱們想要提供給後代組件的方法
          };
        },
        data: function() {
          return {
            map: null
          };
        },
        mounted: function() {
          this.map = new google.maps.Map(this.$el, {
            center: { lat: 0, lng: 0 },
            zoom: 1
          });
        },
        methods: {
          getMap: function(found) {
            var vm = this;
            function checkForMap() {
              if (vm.map) {
                found(vm.map);//獲得map數據
              } else {
                setTimeout(checkForMap, 50);
              }
            }
            checkForMap();
          }
        },
        template: '<div class="map"><slot></slot></div>'
      });

      Vue.component("google-map-marker", {
        inject: ["getMap"],//使用 inject 選項來接收指定的咱們想要添加在這個實例上的 property
        props: ["places"],
        created: function() {
          var vm = this;
          vm.getMap(function(map) {
            vm.places.forEach(function(place) {
              new google.maps.Marker({
                position: place.position,
                map: map
              });
            });
          });
        },
        render(h) {
          return null;
        }
      });

      new Vue({
        el: "#app",
        data: {
          vueConfCities: [
            {
              name: "Wrocław",
              position: {
                lat: 51.107885,
                lng: 17.038538
              }
            },
            {
              name: "New Orleans",
              position: {
                lat: 29.951066,
                lng: -90.071532
              }
            }
          ]
        }
      });
    </script>
複製代碼

訪問子組件實例或子元素

儘管存在 prop 和事件,有的時候你仍可能須要在 JavaScript 裏直接訪問一個子組件。爲了達到這個目的,你能夠經過 ref 這個 attribute 爲子組件賦予一個 ID 引用。

<base-input ref="usernameInput"></base-input>

如今在你已經定義了這個 ref 的組件裏,你可使用:

this.$refs.usernameInput  來訪問這個 <base-input> 實例
複製代碼

假如咱們的目的是從一個父級組件聚焦一個input輸入框,該 <base-input> 組件也可使用一個相似的 ref 提供對內部這個指定元素的訪問。

例如base-input這個組件內部長這樣:
<input ref="input">

也能夠經過其父級組件定義方法:
methods: {
  // 定義在子級中
  // 用來從父級組件聚焦輸入框
  focus: function () {
    this.$refs.input.focus()
  }
}
複製代碼

這樣就容許父級組件經過下面的代碼聚焦 <base-input> 裏的輸入框:

this.$refs.usernameInput.focus()
複製代碼

當 ref 和 v-for 一塊兒使用的時候,你獲得的 ref 將會是一個包含了對應數據源的這些子組件的數組。

注意!$refs 只會在組件渲染完成以後生效,而且它們不是響應式的。這僅做爲一個用於直接操做子組件的「逃生艙」——你應該避免在模板或計算屬性中訪問 $refs

相比 $parent 來講,這個用法可讓咱們在任意後代組件中訪問 getMap,而不須要暴露整個 <google-map> 實例。這容許咱們更好的持續研發該組件,而不須要擔憂咱們可能會改變/移除一些子組件依賴的東西。同時這些組件之間的接口是始終明肯定義的,就和 props 同樣。

然而,依賴注入仍是有負面影響的。它將你應用程序中的組件與它們當前的組織方式耦合起來,使重構變得更加困難。同時所提供的 property 是非響應式的。這是出於設計的考慮,由於使用它們來建立一箇中心化規模化的數據跟使用 $root作這件事都是不夠好的。若是你想要共享的這個 property 是你的應用特有的,而不是通用化的,或者若是你想在祖先組件中更新所提供的數據,那麼這意味着你可能須要換用一個像 Vuex 這樣真正的狀態管理方案了。

有關Vuex的狀態管理學習筆記待填坑。

詳細官方API文檔可看:provide-inject 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()
}
複製代碼

這裏有兩個潛在的問題:

  1. 它須要在這個組件實例中保存這個 picker,若是能夠的話最好只有生命週期鉤子能夠訪問到它。這並不算嚴重的問題,可是它能夠被視爲雜物。
  2. 咱們的創建代碼獨立於咱們的清理代碼,這使得咱們比較難於程序化地清理咱們創建的全部東西。

應該經過一個程序化的偵聽器解決這兩個問題:

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()//一次性偵聽一個事件
    })
  }
}
複製代碼

在這個例子中,咱們推薦建立一個可複用的<input-datepicker> 組件

循環引用

遞歸組件

組件是能夠在它們本身的模板中調用自身的。不過它們只能經過 name 選項來作這件事

name: 'unique-name-of-my-component'
複製代碼

當你使用 Vue.component 全局註冊一個組件時,這個全局的 ID 會自動設置爲該組件的 name 選項。

稍有不慎,遞歸組件就可能致使無限循環

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 的,可是咱們不須要先解析 B。」

把 組件設爲了那個點。咱們知道那個產生悖論的子組件是 組件,因此咱們會等到生命週期鉤子 beforeCreate 時去註冊

beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}
複製代碼

或者,在本地註冊組件的時候,你可使用 webpack 的異步 import

components: {
  TreeFolderContents: () => import('./tree-folder-contents.vue')
}
複製代碼

這樣問題就解決了!

模板定義的替代品

內聯模板

當 inline-template 這個特殊的 attribute 出如今一個子組件上時,這個組件將會使用其裏面的內容做爲模板,而不是將其做爲被分發的內容。這使得模板的撰寫工做更加靈活。

<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 建立低開銷的靜態組件

渲染普通的 HTML 元素在 Vue 中是很是快速的,但有的時候你可能有一個組件,這個組件包含了大量靜態內容。在這種狀況下,你能夠在根元素上添加 v-once attribute 以確保這些內容只計算一次而後緩存起來。

Vue.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})
複製代碼

試着不要過分使用這個模式。當你須要渲染大量靜態內容時,極少數的狀況下它會給你帶來便利,除非你很是留意渲染變慢了,否則它徹底是沒有必要的——再加上它在後期會帶來不少困惑。例如,設想另外一個開發者並不熟悉 v-once 或漏看了它在模板中,他們可能會花不少個小時去找出模板爲何沒法正確更新。變得難以維護。

相關文章
相關標籤/搜索