當我們的項目足夠大,使用的組件就會很多,此時如果一次性加載所有的組件是比較花費時間的。一開始就把所有的組件都加載是沒必要的一筆開銷,此時可以用異步組件來優化一下。
異步組件簡單的說就是只有等到在頁面里顯示該組件的時候才會從服務器加載,不顯式的話就不會加載,這樣即可提高客戶端的訪問速度也可以降低對服務器的請求次數,可謂優化的一個利器。
異步組件常用有3種異步組件的實現:工廠函數、Promise加載和高級異步組件。
注:一般的項目都是在vue-router的路由里面創建vue-router實例時通過routes屬性指定路由的,其實在vue里面也可以實現。
OK,開干,先搭建一個環境,我們先用Vue-li3搭建一個腳手架 ,默認的配置搭建完后在瀏覽器輸入:http://localhost:8080即可打開頁面,默認部分如下:
頁面下部分顯式的就不截圖了,然后點擊about可以切換路由,為了測試我們對異步組件的分析,我們把main.js和app.js和/src/components/HelloWorld.vue進行改寫,如下:
對於/src/components/HelloWorld.vue組件,為了我們測試更方便,直接更改為:
<template> <div class="hello"> <p>Hello World!</p> </div> </template>
只顯示Hello World!就好了,對於main.js文件,修改如下:
修改前的內容為:
import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App) }).$mount('#app')
修改為:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
import helloworld from './components/HelloWorld.vue'
Vue.component('HelloWorld',helloworld)
new Vue({
router,
render: h => h(App)
}).$mount('#app')
修改后HelloWorld作為一個全局的組件形式存在。然后修改app.vue文件
修改前的內容為:
<template> <div id="app"> <div id="nav"> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </div> <router-view/> </div> </template>
我們把它修改為:
<template> <div id="app"> <button @click="show=true">Test</button> <HelloWorld v-if="show"></HelloWorld> </div> </template> <script> export default{ data(){ return{ show:false } } } </script>
渲染后的頁面為:
當我們點擊Test這個按鈕時,Hello World組件就會顯式出來,如下:
這里我們定義的Vue.component('HelloWorld',helloworld)是一個常規組件,非異步組件,下面我們通過修改main.js來模擬不同的異步組件例子,然后通過代碼去看看它的實現原理
一:工廠函數
Vue.js允許將組件定義為一個工廠函數,動態的解析組件,Vue.js只在組件需要渲染時觸發工廠函數,並且把結果緩存起來,用於后面的再次渲染。
例如我們把main.js修改成這樣:
import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false Vue.component('HelloWorld',function(resolve,reject){ //重寫HelloWorld組件的定義 require(['./components/HelloWorld'],function(res){ resolve(res) }) }) new Vue({ router, render: h => h(App) }).$mount('#app')
只有當我們點擊Test這個按鈕時這個組件才會加載進來
源碼分析
當組件執行_render函數轉換成虛擬VNode時遇到組件時會執行createComponent()函數,如下:
function createComponent ( //第4184行 創建組件Vnode Ctor, //Ctor:組件的構造函數 data, //data:數組 context, //context:Vue實例 children, //child:組件的子節點 tag ) { if (isUndef(Ctor)) { return } var baseCtor = context.$options._base; // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor); } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(("Invalid Component definition: " + (String(Ctor))), context); } return } // async component var asyncFactory; if (isUndef(Ctor.cid)) { //如果Ctor.cid為空,那么Ctor就是一個函數,表明這是一個異步組件 asyncFactory = Ctor; //獲取異步組件的函數 Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context); //執行resolveAsyncComponent()函數 if (Ctor === undefined) { //如果Ctor是個空的,調用該函數返回一個空的注釋節點 // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } /*略*/ return vnode }
對於一個組件來說,比如Vue.component(component-name,obj|func),組件的值可以是一個對象,也可以是一個函數,如果是對象,則注冊時會執行Vue.extend()函數,如下:
if (type === 'component' && isPlainObject(definition)) { //第4866行 注冊組件時,如果組件是個對象,則執行Vue.extend() definition.name = definition.name || id; definition = this.options._base.extend(definition); }
去構造子組件的基礎構造函數,此時會在構造函數上新增一個cid屬性(在4789行),所以我們這里通過cid來判斷該組件是否為一個函數。
回到主線,接着執行resolveAsyncComponent()函數,工廠函數相關的如下:
function resolveAsyncComponent ( //第2283行 異步組件 factory:異步組件的函數 baseCtor:大Vue context:當前的Vue實例 factory, baseCtor, context ) { if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp } if (isDef(factory.resolved)) { //工廠函數異步組件第二次執行這里時會返回factory.resolved return factory.resolved } if (isTrue(factory.loading) && isDef(factory.loadingComp)) { return factory.loadingComp } if (isDef(factory.contexts)) { // already pending factory.contexts.push(context); } else { var contexts = factory.contexts = [context]; //將context作為數組保存到contexts里,也就是當前Vue實例 var sync = true; var forceRender = function () { //遍歷contexts里的所有元素 下一個tick執行到這里 for (var i = 0, l = contexts.length; i < l; i++) { //依次調用該元素的$forceUpdate()方法 該方法會強制渲染一次 contexts[i].$forceUpdate(); } }; var resolve = once(function (res) { //定義一個resolve函數 // cache resolved factory.resolved = ensureCtor(res, baseCtor); // invoke callbacks only if this is not a synchronous resolve // (async resolves are shimmed as synchronous during SSR) if (!sync) { forceRender(); } }); var reject = once(function (reason) { //定義一個reject函數 "development" !== 'production' && warn( "Failed to resolve async component: " + (String(factory)) + (reason ? ("\nReason: " + reason) : '') ); if (isDef(factory.errorComp)) { factory.error = true; forceRender(); } }); var res = factory(resolve, reject); //執行factory()函數 if (isObject(res)) { /*高級組件的邏輯*/ } sync = false; // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } }
resolveAsyncComponent內部會定義一個resolve和reject函數,然后執行factory()函數,factory()就是我們在main.js里給HelloWorld組件定義的函數,函數內會執行require函數,由於require()是個異步操作,所以resolveAsyncComponent就會返回undefined
回到resolveAsyncComponent,我們給factory()函數的執行下一個斷點,如下:
可以看到返回一個undefined,最后resolveAsyncComponent()也會返回undefined,回到createComponent()函數,由於返回的是undefined,則會執行createAsyncPlaceholder()去創建一個注釋節點,渲染后對應的DOM節點樹如下:
可以看到對於工廠函數來說,組件完全加載時對應的DOM節點是一個注釋節點
在下一個tick等require()加載成功后就會執行resolve(res)函數,也就是在resolveAsyncComponent()內定義的resolve函數,
resolve函數會將結果保存到工廠函數的resolved屬性里(也就是組件的定義)然后執行的forceRender()函數,也就是上面標記的藍色的注釋對應的代碼
再次重新渲染執行到resolveAsyncComponent的時候此時局部變量factory.resolved存在了,就直接返回該變量, 如下:
此時就會走組件的常規邏輯,進行渲染組件了。
二:Promise加載
Promise()比較簡單,可以認為是工廠函數擴展成語法糖的知識,他主要是可以很好的配合webpack的語法糖,webpack的import的語法糖就是返回一個promise對象,Vue實際上做異步組件也是為了配合Webpack的語法糖來實現Promise()的趨勢。
例如我們把main.js改成如下的:
import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false Vue.component('HelloWorld',()=>import('./components/HelloWorld')) new Vue({ router, render: h => h(App) }).$mount('#app')
和工廠函數一樣,也會執行兩次resolveAsyncComponent,下一個tick的邏輯是一樣的,不一樣的是觸發resolve()的邏輯不通,如下:
源碼分析
function resolveAsyncComponent ( //異步組件 factory, baseCtor, context ) { if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp } if (isDef(factory.resolved)) { //第一次執行到這里時factory.resolved也不存在 return factory.resolved } /*略*/ var res = factory(resolve, reject); //我們這里返回一個含有then的對象 if (isObject(res)) { if (typeof res.then === 'function') { //如果res是一個函數,即Promise()方式加載時 // () => Promise if (isUndef(factory.resolved)) { //如果factory.resolved不存在 res.then(resolve, reject); //用then方法指定resolve和reject的回調函數 } } else if (isDef(res.component) && typeof res.component.then === 'function') { /**/ } } sync = false; // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } }
例子里執行到factory()后返回的res對象如下:
等到加載成功后就會執行resolve了,后面的步驟和工廠函數的流程是一樣的。
三:高級異步組件
高級異步組件可以定義更多的狀態,比如加載該組件的超時時間、加載過程中顯式的組件、出錯時顯式的組件、延遲時間等
高級異步組件也是定義一個函數,返回值是一個對象,對象的每個屬性在官網說得挺詳細的了,如下,連接::https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%A4%84%E7%90%86%E5%8A%A0%E8%BD%BD%E7%8A%B6%E6%80%81
對於高級異步組件來說,他和promise()方法加載的邏輯是一樣的,不同的是多了幾個屬性,如下:
源碼分析
function resolveAsyncComponent ( //第2283行 異步組件 factory, baseCtor, context ) { /*略*/ if (isObject(res)) { if (typeof res.then === 'function') { //promise的分支 // () => Promise if (isUndef(factory.resolved)) { res.then(resolve, reject); } } else if (isDef(res.component) && typeof res.component.then === 'function') { //高級異步組件的分支 res.component.then(resolve, reject); //還是調用res.component.then(resolve, reject); 進行處理的,不同的是多了下面的代碼 if (isDef(res.error)) { //失敗時的模塊 factory.errorComp = ensureCtor(res.error, baseCtor); } if (isDef(res.loading)) { //如果有設置加載時的模塊 factory.loadingComp = ensureCtor(res.loading, baseCtor); if (res.delay === 0) { //如果等待時間為0 factory.loading = true; //直接設置factory.loading為true } else { setTimeout(function () { if (isUndef(factory.resolved) && isUndef(factory.error)) { factory.loading = true; forceRender(); } }, res.delay || 200); } } if (isDef(res.timeout)) { //超時時間 setTimeout(function () { if (isUndef(factory.resolved)) { reject( process.env.NODE_ENV !== 'production' ? ("timeout (" + (res.timeout) + "ms)") : null ); } }, res.timeout); } } } sync = false; // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } }
OK,搞定,流程就這樣吧