Vue官方文檔提供註冊異步組件的方式有三種:vue
咱們看下 Vue 官方文檔提供的示例:node
Vue.component('async-webpack-example', function (resolve) { // 這個特殊的 `require` 語法將會告訴 webpack // 自動將你的構建代碼切割成多個包, 這些包 // 會經過 Ajax 請求加載 require(['./my-async-component'], resolve) })
簡單說明一下, 這個示例調用 Vue 的靜態方法 component 實現組件註冊, 須要瞭解下 Vue.component 的大體實現webpack
// 此時type爲component Vue[type] = function ( id: string, definition: Function | Object ): Function | Object | void { if (!definition) { return this.options[type + 's'][id] } else { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && type === 'component') { validateComponentName(id) } // 是否爲對象 if (type === 'component' && isPlainObject(definition)) { definition.name = definition.name || id definition = this.options._base.extend(definition) } if (type === 'directive' && typeof definition === 'function') { definition = { bind: definition, update: definition } } // 記錄當前Vue的全局components, filters, directives對應的聲明映射 this.options[type + 's'][id] = definition return definition } }
先判斷傳入的 definition 也就是咱們的工廠函數, 是否爲對象, 都說是工廠函數了, 那確定不爲對象, 因而這裏不調用 this.options._base.extend(definition) 來獲取組件的構造函數, 而是直接把當前的 definition(工廠函數) 保存到 this.options.components 的 'async-webpack-example' 屬性值中, 並返回definition。web
接下來發生什麼事情呢?
其實剛纔咱們只是調用了 Vue.component 註冊一個異步組件, 可是咱們最終是經過 new Vue 實例來實現頁面的渲染。這裏大體瀏覽一下渲染的過程:異步
Run:async
因此咱們如今以及進入到 createComponent 這個函數了, 看下這裏異步組件具體的實現邏輯:函數
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, // vm實例 children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // 在init初始化的時候賦值Vue const baseCtor = context.$options._base // Ctor當前爲異步組件的工廠函數, 因此此步驟不執行 if (isObject(Ctor)) { // 獲取構造器, 對於非全局註冊的組件使用 Ctor = baseCtor.extend(Ctor) } // async component let asyncFactory // 若是Ctro.cid爲undefined, 則說明h會是異步組件註冊 // 緣由是沒有調用過 Vue.extend 進行組件構造函數轉換獲取 if (isUndef(Ctor.cid)) { asyncFactory = Ctor // 解析異步組件 Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // Ctor爲undefined則直接建立並返回異步組件的佔位符組件Vnode if (Ctor === undefined) { return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } ...此處省略不分析的代碼 // 安裝組件的鉤子 installComponentHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, // 組件對象componentOptions asyncFactory ) return vnode }
從源碼咱們能夠看出, 異步組件不執行組件構造器的轉換獲取, 而是執行 resolveAsyncComponent 來獲取返回的組件構造器。因爲該過程是異步請求組件, 因此咱們看下 resolveAsyncComponent 的實現ui
// 定義在render.js中的全局變量, 用於記錄當前正在渲染的vm實例 import { currentRenderingInstance } from 'core/instance/render' export function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void { // 高級異步組件使用 if (isTrue(factory.error) && isDef(factory.errorComp)) {...先省略} if (isDef(factory.resolved)) { return factory.resolved } // 獲取當前正在渲染的vm實例 const owner = currentRenderingInstance if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) { // already pending factory.owners.push(owner) } if (isTrue(factory.loading) && isDef(factory.loadingComp)) {...省略} // 執行該邏輯 if (owner && !isDef(factory.owners)) { const owners = factory.owners = [owner] // 用於標記是否 let sync = true ...省略 const forceRender = (renderCompleted: boolean) => { ...省略 } // once讓被once包裝的任何函數的其中一個只執行一次 const resolve = once((res: Object | Class<Component>) => { // 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(true) } else { owners.length = 0 } }) const reject = once(reason => { ...省略 }) // 執行工廠函數, 好比webpack獲取異步組件資源 const res = factory(resolve, reject) ...省略 sync = false // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } }
resolveAsyncComponent 傳入異步組件工廠函數和 baseCtor(也就是Vue.extend), 先獲取當前渲染的vm實例接着標記sync爲true, 表示當前爲執行同步代碼階段, 定義 resolve 和 reject 函數(忽略不分析), 此時咱們能夠發現 resolve 和 reject 都被 once 函數所封裝, 目的是讓被 once 包裝的任何函數的其中一個只執行一次, 保證 resolve 和 reject 二者只能擇一併只執行一次。OK, 接着來到 factory 的執行, 其實就是執行官方示例中傳入的工廠函數, 這時候發起異步組件的請求。同步代碼繼續執行, sync置位false, 表示當前的同步代碼執行完畢, 而後返回undefinedthis
這裏可能會問怎麼會返回undefined, 由於咱們傳入的工廠函數沒有loading屬性, 而後當前的 factory 也沒有 resolved 屬性。spa
接着回到 createComponent 的代碼中:
if (isUndef(Ctor.cid)) { asyncFactory = Ctor // 解析異步組件 Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // Ctor爲undefined則直接建立並返回異步組件的佔位符組件Vnode if (Ctor === undefined) { return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } }
由於剛纔說 resolveAsyncComponent 執行返回了undefined, 因此執行 createAsyncPlaceholder 建立註釋vnode
這裏可能還會問爲何要建立一個註釋vnode, 提早揭曉答案:
由於先要返回一個佔位的 vnode, 等待異步請求加載後執行 forceUpdate 從新渲染, 而後這個節點會被更新渲染成組件的節點。
那繼續, 剛纔答案說了, 當異步組件請求完成後, 則執行 resolve 並傳入對應的異步組件, 這時候 factory.resolved 被賦值爲 ensureCtor 執行的返回結果, 就是一個組件構造器, 而後這時候 sync 爲 false, 因此執行 forceRender, 而 forceRender 其實就是調用 vm.$forceUpdate 實現以下:
Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } }
$forceUpdate 執行渲染 watcher 的 update 方法, 因而咱們又會執行 createComponent 的方法, 執行 resolveAsyncComponent, 這時候 factory.resolved 已經定義過了, 因而直接返回 factory.resolved 的組件構造器。 因而就執行 createComponent 的後續組件的渲染和 patch 邏輯了。組件渲染和 patch 這裏先不展開。
因而整個異步組件的流程就結束了。
先看下官網文檔提供的示例:
Vue.component( 'async-webpack-example', // 這個 `import` 函數會返回一個 `Promise` 對象。 () => import('./my-async-component') )
由上面的示例, 能夠看到當調用Vue.component的時候, definition爲一個會返回 Promise 的函數, 與工廠函數執行 resolve 回調不一樣的地方在於:
export function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void { ...省略 // 執行工廠函數, 好比webpack獲取異步組件資源 const res = factory(resolve, reject) if (isObject(res)) { // 爲Promise對象, import('./async-component') if (isPromise(res)) { // () => Promise if (isUndef(factory.resolved)) { res.then(resolve, reject) } } else if (isPromise(res.component)) { 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) { factory.loading = true } else { timerLoading = setTimeout(() => { timerLoading = null if (isUndef(factory.resolved) && isUndef(factory.error)) { factory.loading = true forceRender(false) } }, res.delay || 200) } } if (isDef(res.timeout)) { timerTimeout = setTimeout(() => { timerTimeout = null 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 } }
主要不一樣點在於執行完 factory 工廠函數, 這時候咱們的工廠函數會返回一個 Promise, 因此 res.then(resolve, reject) 會執行, 接下來的過程也是等待異步組件請求完成, 而後執行 resolve 函數, 接着執行 forceRender 而後返回組件構造器。
這裏 Promise 寫法的異步組件註冊過程和執行回調函數沒有太大的區別。
一樣, 看下官網示例:
const AsyncComponent = () => ({ // 須要加載的組件 (應該是一個 `Promise` 對象) component: import('./MyComponent.vue'), // 異步組件加載時使用的組件 loading: LoadingComponent, // 加載失敗時使用的組件 error: ErrorComponent, // 展現加載時組件的延時時間。默認值是 200 (毫秒) delay: 200, // 若是提供了超時時間且組件加載也超時了, // 則使用加載失敗時使用的組件。默認值是:`Infinity` timeout: 3000 })
從上面的示例能夠看到, 工廠函數在執行成功後會返回一個配置對象, 這個對象的5個屬性咱們均可以從官方文檔的註釋瞭解到各自的做用。那咱們看一下這種方式和前面提到的兩種方式的區別在哪裏.
export function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void { // 高級異步組件使用 if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp } if (isDef(factory.resolved)) { return factory.resolved } ...已瞭解過,省略 if (isTrue(factory.loading) && isDef(factory.loadingComp)) { return factory.loadingComp } if (owner && !isDef(factory.owners)) { const owners = factory.owners = [owner] let sync = true let timerLoading = null let timerTimeout = null ;(owner: any).$on('hook:destroyed', () => remove(owners, owner)) const forceRender = (renderCompleted: boolean) => {...省略} // once讓被once包裝的任何函數的其中一個只執行一次 const resolve = once((res: Object | Class<Component>) => { factory.resolved = ensureCtor(res, baseCtor) if (!sync) { forceRender(true) } else { owners.length = 0 } }) const reject = once(reason => { process.env.NODE_ENV !== 'production' && warn( `Failed to resolve async component: ${String(factory)}` + (reason ? `\nReason: ${reason}` : '') ) if (isDef(factory.errorComp)) { factory.error = true forceRender(true) } }) // 執行工廠函數,好比webpack獲取異步組件資源 const res = factory(resolve, reject) if (isObject(res)) { // 爲Promise對象, import('./async-component') if (isPromise(res)) { ...省略 } else if (isPromise(res.component)) { 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) { factory.loading = true } else { timerLoading = setTimeout(() => { timerLoading = null if (isUndef(factory.resolved) && isUndef(factory.error)) { factory.loading = true forceRender(false) } }, res.delay || 200) } } if (isDef(res.timeout)) { timerTimeout = setTimeout(() => { timerTimeout = null 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 } }
渲染過程一樣來到 resolveAsyncComponent, 一開始判斷 factory.error 是否爲 true, 固然一開始確定是 false 的, 不進入該邏輯, 接着一樣執行到 const res = factory(resolve, reject) 的執行, 由於咱們剛纔說了咱們的工廠函數返回了一個異步組件配置對象, 因而 res 就是咱們定義該工廠函數返回的對象, 這時候 isObject(res) 爲 true, isPromise(res) 爲 false, isPromise(res.component) 爲 true, 接着判斷 res.error 是否有定義, 因而在 factory 定義擴展了 errorComp, errorComp是經過 ensureCtor 來對 res.error 的定義組件轉化爲組件的構造器, loading 也是同樣的邏輯, 在 factory 擴展 loadingComp 組件構造器。
接着, 這時候須要特別注意, 當咱們定義的 res.delay 爲 0, 則直接把 factory.loading 置爲 true, 由於這裏影響到 resolveAsyncComponent 的返回值。
return factory.loading ? factory.loadingComp : factory.resolved
當 factory.loading 爲 true, 會返回 loadingComp, 使得 createComponet 的時候不是建立一個註釋vnode, 而是直接執行 loadingComp 的渲染。
若是咱們的 res.delay 不爲0, 則會啓用一個計時器, 先同步返回 undefined 觸發註釋節點建立, 在必定的時間後執行 factory.loading = true 和 forceRender(false), 條件是組件沒有加載完成以及沒有出錯 reject, 接着執行把註釋vnode 替換爲加載過程組件 loadingComp 的渲染。
而 res.timeout 主要用來計時, 當在 res.timeout 的時間內, 若是當前的 factory.resolved 爲 undefined, 則說明異步組件加載已經超時了, 因而會調用 reject 方法, reject 其實就是調用 forceRender 來執行 errorComp 的渲染。
OK, 當咱們的組件加載完成了, 執行了 resolve 方法, factory.resloved 置爲 true, 調用 forceRender 來把註釋節點或者是 loadingComp 的節點替換渲染爲加載完成的組件。
到此, 咱們已經瞭解三種異步組件的註冊過程了。
異步組件的渲染本質上其實就是執行2次或者2次以上的渲染, 先把當前組件渲染爲註釋節點, 當組件加載成功後, 經過 forceRender 執行從新渲染。或者是渲染爲註釋節點, 而後再渲染爲loading節點, 在渲染爲請求完成的組件。
這裏須要注意的是 forceRender 的執行, forceRender 用於強制執行當前的節點從新渲染, 至於整個渲染過程是怎麼樣的後續文章有機會的話。。。再講解吧。
本人語文表達能力有限, 只是突發奇想爲了把本身瞭解到的過程用本身的話語表達出來, 若是有什麼錯誤的地方望多多包涵。