一、生命週期
- 每個
Vue
實例在被創建之前都要經過一系列的初始化過程。例如需要設置數據監聽、編譯模板、掛載實例到DOM
、在數據變化時更新DOM
等。同時在這個過程中也會運行一些叫做生命週期鉤子的函數,給予用戶機會在一些特定的場景下添加他們自己的代碼。在我們實際項目開發過程中,會非常頻繁地和Vue
組件的生命週期打交道,接下來我們就從源碼的角度來看一下這些生命週期的鉤子函數是如何被執行的。源碼中最終執行生命週期的函數都是調用callHook
方法,它的定義在src/core/instance/lifecycle
中:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
callHook
函數的邏輯很簡單,根據傳入的字符串hook
,去拿到vm.$options[hook]
對應的回調函數數組,然後遍歷執行,執行的時候把vm
作爲函數執行的上下文。
-
在之前詳細地介紹了
Vue.js
合併options
的過程,各個階段的生命週期的函數也被合併到vm.$options
裏,並且是一個數組。因此callhook
函數的功能就是調用某個生命週期鉤子註冊的所有回調函數。瞭解了生命週期的執行方式後,接下來會具體介紹每一個生命週期函數它的調用時機。 -
beforeCreate & created
,如下所示:
beforeCreate
和created
函數都是在實例化Vue
的階段,在_init
方法中執行的,它的定義在src/core/instance/init.js
中:
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
-
可以看到
beforeCreate
和created
的鉤子調用是在initState
的前後,initState
的作用是初始化props
、data
、methods
、watch
、computed
等屬性,。那麼顯然beforeCreate
的鉤子函數中就不能獲取到props
、data
中定義的值,也不能調用methods
中定義的函數。 -
在這倆個鉤子函數執行的時候,並沒有渲染
DOM
,所以我們也不能夠訪問DOM
,一般來說,如果組件在加載的時候需要和後端有交互,放在這倆個鉤子函數執行都可以,如果是需要訪問props
、data
等數據的話,就需要使用created
鉤子函數,之後發現它們都混合了beforeCreate
鉤子函數。
beforeMount & mounted
,如下所示:
beforeMount
鉤子函數發生在mount
,也就是DOM
掛載之前,它的調用時機是在mountComponent
函數中,定義在src/core/instance/lifecycle.js
中:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// ...
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
- 在執行
vm._render()
函數渲染VNode
之前,執行了beforeMount
鉤子函數,在執行完vm._update()
把VNode patch
到真實DOM
後,執行mounted
鉤子。注意,這裏對mounted
鉤子函數執行有一個判斷邏輯,vm.$vnode
如果爲null
,則表明這不是一次組件的初始化過程,而是我們通過外部new Vue
初始化過程。 - 組件的
VNode patch
到DOM
後,會執行invokeInsertHook
函數,把insertedVnodeQueue
裏保存的鉤子函數依次執行一遍,它的定義在src/core/vdom/patch.js
中:
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
- 該函數會執行
insert
這個鉤子函數,對於組件而言,insert
鉤子函數的定義在src/core/vdom/create-component.js
中的componentVNodeHooks
中:
const componentVNodeHooks = {
// ...
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
// ...
},
}
- 我們可以看到,每個子組件都是在這個鉤子函數中執行
mounted
鉤子函數,並且我們之前分析過,insertedVnodeQueue
的添加順序是先子後父,所以對於同步渲染的子組件而言,mounted
鉤子函數的執行順序也是先子後父。
beforeUpdate & updated
,如下所示:
beforeUpdate
和updated
的鉤子函數執行時機都應該是在數據更新的時候,beforeUpdate
的執行時機是在渲染Watcher
的before
函數中,我們剛纔提到過:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}
-
注意這裏有個判斷,也就是在組件已經
mounted
之後,纔會去調用這個鉤子函數。 -
update
的執行時機是在flushSchedulerQueue
函數調用的時候,它的定義在src/core/observer/scheduler.js
中:
function flushSchedulerQueue () {
// ...
// 獲取到 updatedQueue
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
-
flushSchedulerQueue
函數我們之後會詳細介紹,updatedQueue
是更新了的wathcer
數組,那麼在callUpdatedHooks
函數中,它對這些數組做遍歷,只有滿足當前watcher
爲vm._watcher
以及組件已經mounted
這兩個條件,纔會執行updated
鉤子函數。 -
我們之前提過,在組件
mount
的過程中,會實例化一個渲染的Watcher
去監聽vm
上的數據變化重新渲染,這段邏輯發生在mountComponent
函數執行的時候:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
// 這裏是簡寫
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}
- 那麼在實例化
Watcher
的過程中,在它的構造函數裏會判斷isRenderWatcher
,接着把當前watcher
的實例賦值給vm._watcher
,定義在src/core/observer/watcher.js
中:
export default class Watcher {
// ...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
}
}
- 同時,還把當前
wathcer
實例push
到vm._watchers
中,vm._watcher
是專門用來監聽vm
上數據變化然後重新渲染的,所以它是一個渲染相關的watcher
,因此在callUpdatedHooks
函數中,只有vm._watcher
的回調執行完畢後,纔會執行updated
鉤子函數。
beforeDestroy & destroyed
,如下所示:
beforeDestroy
和destroyed
鉤子函數的執行時機在組件銷燬的階段,最終會調用$destroy
方法,它的定義在src/core/instance/lifecycle.js
中:
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
-
beforeDestroy
鉤子函數的執行時機是在$destroy
函數執行最開始的地方,接着執行了一系列的銷燬動作,包括從parent
的$children
中刪掉自身,刪除watcher
,當前渲染的VNode
執行銷燬鉤子函數等,執行完畢後再調用destroy
鉤子函數。 -
在
$destroy
的執行過程中,它又會執行vm.__patch__(vm._vnode, null)
觸發它子組件的銷燬鉤子函數,這樣一層層的遞歸調用,所以destroy
鉤子函數執行順序是先子後父,和mounted
過程一樣。
-
activated & deactivated
,activated
和deactivated
鉤子函數是專門爲keep-alive
組件定製的鉤子,在介紹keep-alive
組件的時候會詳細介紹。 -
總結:
Vue
生命週期中各個鉤子函數的執行時機以及順序,通過分析,我們知道了如在created
鉤子函數中可以訪問到數據,在mounted
鉤子函數中可以訪問到DOM
,在destroy
鉤子函數中可以做一些定時器銷燬工作,瞭解它們有利於我們在合適的生命週期去做不同的事情。
二、組件註冊
- 在
Vue.js
中,除了它內置的組件如keep-alive
、component
、transition
、transition-group
等,其它用戶自定義組件在使用前必須註冊,在開發過程中可能會遇到如下報錯信息:
'Unknown custom element: <xxx> - did you register the component correctly?
For recursive components, make sure to provide the "name" option.'
一般報這個錯的原因都是我們使用了未註冊的組件。Vue.js 提供了兩種組件的註冊方式,全局註冊和局部註冊。接下來我們從源碼分析的角度來分析這兩種註冊方式。
- 全局註冊,要註冊一個全局組件,可以使用
Vue.component(tagName, options)
。例如:
Vue.component('my-component', {
// 選項
})
- 那麼,
Vue.component
函數的定義過程發生是在最開始初始化Vue
的全局函數的時候,代碼在src/core/global-api/assets.js
中,如下所示:
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
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 }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
- 函數首先遍歷
ASSET_TYPES
,得到type
後掛載到Vue
上,ASSET_TYPES
的定義在src/shared/constants.js
中:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
-
所以實際上
Vue
是初始化了三個全局函數,並且如果type
是component
且definition
是一個對象的話,通過this.opitons._base.extend
, 相當於Vue.extend
把這個對象轉換成一個繼承於Vue
的構造函數,最後通過this.options[type + 's'][id] = definition
把它掛載到Vue.options.components
上。 -
由於我們每個組件的創建都是通過
Vue.extend
繼承而來,我們之前分析過在繼承的過程中有這麼一段邏輯:
Sub.options = mergeOptions(
Super.options,
extendOptions
)
- 這也就是說它會把
Vue.options
合併到Sub.options
,也就是組件的options
上, 然後在組件的實例化階段,會執行merge options
邏輯,把Sub.options.components
合併到vm.$options.components
上。然後在創建vnode
的過程中,會執行_createElement
方法,我們再來回顧一下這部分的邏輯,它的定義在src/core/vdom/create-element.js
中:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
// ...
}
這裏有一個判斷邏輯
isDef(Ctor = resolveAsset(context.$options, 'components', tag))
,先來看一下resolveAsset
的定義,在src/core/utils/options.js
中:
/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*/
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
-
這段邏輯很簡單,先通過
const assets = options[type]
拿到assets
,然後再嘗試拿assets[id]
,這裏有個順序,先直接使用id
拿,如果不存在,則把id
變成駝峯的形式再拿,如果仍然不存在則在駝峯的基礎上把首字母再變成大寫的形式再拿,如果仍然拿不到則報錯。這樣說明了我們在使用Vue.component(id, definition)
全局註冊組件的時候,id
可以是連字符、駝峯或首字母大寫的形式。 -
那麼回到我們的調用
resolveAsset(context.$options, 'components', tag)
,即拿vm.$options.components[tag]
,這樣我們就可以在resolveAsset
的時候拿到這個組件的構造函數,並作爲createComponent
的鉤子的參數。 -
局部註冊,
Vue.js
也同樣支持局部註冊,我們可以在一個組件內部使用components
選項做組件的局部註冊,例如:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
-
其實理解了全局註冊的過程,局部註冊是非常簡單的。在組件的
Vue
的實例化階段有一個合併option
的邏輯,之前我們也分析過,所以就把components
合併到vm.$options.components
上,這樣我們就可以在resolveAsset
的時候拿到這個組件的構造函數,並作爲createComponent
的鉤子的參數。 -
注意,局部註冊和全局註冊不同的是,只有該類型的組件纔可以訪問局部註冊的子組件,而全局註冊是擴展到
Vue.options
下,所以在所有組件創建的過程中,都會從全局的Vue.options.components
擴展到當前組件的vm.$options.components
下,這就是全局註冊的組件能被任意使用的原因。 -
總結:我們對組件的註冊過程有了認識,並理解了全局註冊和局部註冊的差異。其實在平時的工作中,當我們使用到組件庫的時候,往往更通用基礎組件都是全局註冊的,而編寫的特例場景的業務組件都是局部註冊的。瞭解了它們的原理,對我們在工作中到底使用全局註冊組件還是局部註冊組件是有這非常好的指導意義的。
三、異步組件
- 在我們平時的開發工作中,爲了減少首屏代碼體積,往往會把一些非首屏的組件設計成異步組件,按需加載。
Vue
也原生支持了異步組件的能力,如下所示:
Vue.component('async-example', function (resolve, reject) {
// 這個特殊的 require 語法告訴 webpack
// 自動將編譯後的代碼分割成不同的塊,
// 這些塊將通過 Ajax 請求自動下載。
require(['./my-async-component'], resolve)
})
-
在示例中可以看到,
Vue
註冊的組件不再是一個對象,而是一個工廠函數,函數有兩個參數resolve
和reject
,函數內部用setTimout
模擬了異步,實際使用可能是通過動態請求異步組件的JS
地址,最終通過執行resolve
方法,它的參數就是我們的異步組件對象。 -
在瞭解了異步組件如何註冊後,我們從源碼的角度來分析一下它的實現。之前我們分析了組件的註冊邏輯,由於組件的定義並不是一個普通對象,所以不會執行
Vue.extend
的邏輯把它變成一個組件的構造函數,但是它仍然可以執行到createComponent
函數,我們再來對這個函數做回顧,它的定義在src/core/vdom/create-component/js
中:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// ...
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
// 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
)
}
}
}
- 由於我們這個時候傳入的
Ctor
是一個函數,那麼它也並不會執行Vue.extend
邏輯,因此它的cid
是undefiend
,進入了異步組件創建的邏輯。這裏首先執行了Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
方法,它的定義在src/core/vdom/helpers/resolve-async-component.js
中:
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>,
context: 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 (isDef(factory.contexts)) {
// already pending
factory.contexts.push(context)
} else {
const contexts = factory.contexts = [context]
let sync = true
const forceRender = () => {
for (let i = 0, l = contexts.length; i < l; i++) {
contexts[i].$forceUpdate()
}
}
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()
}
})
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()
}
})
const res = factory(resolve, reject)
if (isObject(res)) {
if (typeof res.then === 'function') {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isDef(res.component) && typeof res.component.then === 'function') {
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 {
setTimeout(() => {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender()
}
}, res.delay || 200)
}
}
if (isDef(res.timeout)) {
setTimeout(() => {
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
函數的邏輯略複雜,因爲它實際上處理了三種異步組件的創建方式,除了剛纔示例的組件註冊方式,還支持兩種,一種是支持Promise
創建組件的方式,如下所示:
Vue.component(
'async-webpack-example',
// 該 `import` 函數返回一個 `Promise` 對象。
() => import('./my-async-component')
)
另一種是高級異步組件,如下所示:
const AsyncComp = () => ({
// 需要加載的組件。應當是一個 Promise
component: import('./MyComp.vue'),
// 加載中應當渲染的組件
loading: LoadingComp,
// 出錯時渲染的組件
error: ErrorComp,
// 渲染加載中組件前的等待時間。默認:200ms。
delay: 200,
// 最長等待時間。超出此時間則渲染錯誤組件。默認:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)
那麼我們就根據這三種異步組件的情況,來分別去分析
resolveAsyncComponent
的邏輯。
- 普通函數異步組件,針對普通函數的情況,前面幾個 if 判斷可以忽略,它們是爲高級組件所用,對於
factory.contexts
的判斷,是考慮到多個地方同時初始化一個異步組件,那麼它的實際加載應該只有一次。接着進入實際加載邏輯,定義了forceRender
、resolve
和reject
函數,注意resolve
和reject
函數用once
函數做了一層包裝,它的定義在src/shared/util.js
中:
/**
* Ensure a function is called only once.
*/
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
once
邏輯非常簡單,傳入一個函數,並返回一個新函數,它非常巧妙地利用閉包和一個標誌位保證了它包裝的函數只會執行一次,也就是確保resolve
和reject
函數只執行一次。
- 接下來執行
const res = factory(resolve, reject)
邏輯,這塊兒就是執行我們組件的工廠函數,同時把resolve
和reject
函數作爲參數傳入,組件的工廠函數通常會先發送請求去加載我們的異步組件的JS
文件,拿到組件定義的對象res
後,執行resolve(res)
邏輯,它會先執行factory.resolved = ensureCtor(res, baseCtor)
:
function ensureCtor (comp: any, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
return isObject(comp)
? base.extend(comp)
: comp
}
這個函數目的是爲了保證能找到異步組件 JS 定義的組件對象,並且如果它是一個普通對象,則調用
Vue.extend
把它轉換成一個組件的構造函數。
resolve
邏輯最後判斷了sync
,顯然我們這個場景下sync
爲false
,那麼就會執行forceRender
函數,它會遍歷factory.contexts
,拿到每一個調用異步組件的實例vm
, 執行vm.$forceUpdate()
方法,它的定義在src/core/instance/lifecycle.js
中:
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
-
$forceUpdate
的邏輯非常簡單,就是調用渲染watcher
的update
方法,讓渲染watcher
對應的回調函數執行,也就是觸發了組件的重新渲染。之所以這麼做是因爲Vue
通常是數據驅動視圖重新渲染,但是在整個異步組件加載過程中是沒有數據發生變化的,所以通過執行$forceUpdate
可以強制組件重新渲染一次。 -
Promise
異步組件,如下所示:
Vue.component(
'async-webpack-example',
// 該 `import` 函數返回一個 `Promise` 對象。
() => import('./my-async-component')
)
webpack 2+
支持了異步加載的語法糖:() => import('./my-async-component')
,當執行完res = factory(resolve, reject)
,返回的值就是import('./my-async-component')
的返回值,它是一個Promise
對象。接着進入if
條件,又判斷了typeof res.then === 'function')
,條件滿足,執行:
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
當組件異步加載成功後,執行
resolve
,加載失敗則執行reject
,這樣就非常巧妙地實現了配合 webpack 2+ 的異步加載組件的方式(Promise
)加載異步組件。
- 高級異步組件,由於異步加載組件需要動態加載
JS
,有一定網絡延時,而且有加載失敗的情況,所以通常我們在開發異步組件相關邏輯的時候需要設計loading
組件和error
組件,並在適當的時機渲染它們。Vue.js 2.3+
支持了一種高級異步組件的方式,它通過一個簡單的對象配置,幫你搞定loading
組件和error
組件的渲染時機,你完全不用關心細節,非常方便。接下來我們就從源碼的角度來分析高級異步組件是怎麼實現的,如下所示:
const AsyncComp = () => ({
// 需要加載的組件。應當是一個 Promise
component: import('./MyComp.vue'),
// 加載中應當渲染的組件
loading: LoadingComp,
// 出錯時渲染的組件
error: ErrorComp,
// 渲染加載中組件前的等待時間。默認:200ms。
delay: 200,
// 最長等待時間。超出此時間則渲染錯誤組件。默認:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)
-
高級異步組件的初始化邏輯和普通異步組件一樣,也是執行
resolveAsyncComponent
,當執行完res = factory(resolve, reject)
,返回值就是定義的組件對象,顯然滿足else if (isDef(res.component) && typeof res.component.then === 'function')
的邏輯,接着執行res.component.then(resolve, reject)
,當異步組件加載成功後,執行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 {
setTimeout(() => {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender()
}
}, res.delay || 200)
}
}
if (isDef(res.timeout)) {
setTimeout(() => {
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
先判斷
res.error
是否定義了 error 組件,如果有的話則賦值給factory.errorComp
。
接着判斷res.loading
是否定義了 loading 組件,如果有的話則賦值給factory.loadingComp
,如果設置了res.delay
且爲 0,則設置factory.loading = true
,否則延時delay
的時間執行,如下所示:
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender()
}
最後判斷
res.timeout
,如果配置了該項,則在res.timout
時間後,如果組件沒有成功加載,執行reject
。
- 在
resolveAsyncComponent
的最後有一段邏輯,如下所示:
sync = false
return factory.loading
? factory.loadingComp
: factory.resolved
-
如果
delay
配置爲0
,則這次直接渲染loading
組件,否則則延時delay
執行forceRender
,那麼又會再一次執行到resolveAsyncComponent
。那麼這時候我們有幾種情況,按邏輯的執行順序,對不同的情況做判斷。 -
異步組件加載失敗,當異步組件加載失敗,會執行
reject
函數:
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()
}
})
這個時候會把
factory.error
設置爲true
,同時執行forceRender()
再次執行到resolveAsyncComponent
:
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
那麼這個時候就返回
factory.errorComp
,直接渲染 error 組件。
- 異步組件加載成功,當異步組件加載成功,會執行
resolve
函數:
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender()
}
})
首先把加載結果緩存到
factory.resolved
中,這個時候因爲sync
已經爲 false,則執行forceRender()
再次執行到resolveAsyncComponent
:
if (isDef(factory.resolved)) {
return factory.resolved
}
那麼這個時候直接返回
factory.resolved
,渲染成功加載的組件。
- 異步組件加載中,如果異步組件加載中並未返回,這時候會走到這個邏輯:
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
那麼則會返回
factory.loadingComp
,渲染 loading 組件。
-
異步組件加載超時,如果超時,則走到了
reject
邏輯,之後邏輯和加載失敗一樣,渲染error
組件。 -
異步組件
patch
,回到createComponent
的邏輯:
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
- 如果是第一次執行
resolveAsyncComponent
,除非使用高級異步組件0 delay
去創建了一個loading
組件,否則返回是undefiend
,接着通過createAsyncPlaceholder
創建一個註釋節點作爲佔位符。它的定義在src/core/vdom/helpers/resolve-async-components.js
中:
export function createAsyncPlaceholder (
factory: Function,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag: ?string
): VNode {
const node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = { data, context, children, tag }
return node
}
實際上就是就是創建了一個佔位的註釋 VNode,同時把
asyncFactory
和asyncMeta
賦值給當前vnode
。
-
當執行
forceRender
的時候,會觸發組件的重新渲染,那麼會再一次執行resolveAsyncComponent
,這時候就會根據不同的情況,可能返回loading、error
或成功加載的異步組件,返回值不爲undefined
,因此就走正常的組件render
、patch
過程,與組件第一次渲染流程不一樣,這個時候是存在新舊vnode
的,後面會分析組件更新的patch
過程。 -
總結:我們對
Vue
的異步組件的實現有了深入的瞭解,知道了三種異步組件的實現方式,並且看到高級異步組件的實現是非常巧妙的,它實現了loading、resolve、reject、timeout
四種狀態。異步組件實現的本質是兩次渲染,除了0 delay
的高級異步組件第一次直接渲染成loading
組件外,其它都是第一次渲染生成一個註釋節點,當異步獲取組件成功後,再通過forceRender
強制重新渲染,這樣就能正確渲染出我們異步加載的組件了。