Vue中总共有3种异步组件,分别是普通函数异步组件、Promise异步组件和高级异步组件,接下来分别介绍一下它们的原理
普通函数异步组件
Vue.component('hello-world', function (resolve, reject) {
require(['../components/HelloWorld'], resolve)
})
复制代码
这个require
语法的执行逻辑是,先请求这个组件,请求成功后调用resolve
函数。
先看下Vue.component
的实现,在创建Vue函数时,会执行initGlobalAPI
,里面会执行initAssetRegisters
方法
// 注册 Vue.component,Vue.directive, Vue.filter 函数
initAssetRegisters(Vue)
复制代码
initAssetRegisters
定义在src/core/global-api/assets.js
中
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (id: string, definition: Function | Object) {}
})
}
复制代码
initAssetRegisters
方法会分别注册Vue.component
、Vue.directive
和 Vue.filter
。
当通过Vue.component
注册组件时,执行这个方法
function (id: string, definition: Function | Object) {
if (!definition) {
return this.options[type + 's'][id]
} else {
if (type === 'component' && isPlainObject(definition)) {
// 全局注册的组件,如果是一个对象,则通过 Vue.extend 返回一个子组件的构造函数
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
}
}
复制代码
首先如果没有传入definition
则去this.options.components
中查找这个组件并返回;反之如果definition
是一个普通对象,则通过Vue.extend
返回一个子组件的构造函数;并将这个子组件构造函数添加到this.options.components
中
当注册普通函数异步组件时,由于传入的是一个函数,所以不会创建组件构造函数,而是直接将传入的函数挂载到Vue.options.components
中。
当调用createComponent
创建组件VNode时,有如下逻辑
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
复制代码
如果传入的Ctor
是对象,创建组件构造函数;如果Ctor.cid
没有值说明Ctor
是一个自定义函数(大概率是异步组件),因为如果传入的是一个对象会通过Vue.extend
创建组件构造函数,并有一个cid
属性。
接下来调用resolveAsyncComponent
方法
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
// ...
// currentRenderingInstance 在 _render 函数中被赋值,指向当前的 Vue 实例
const owner = currentRenderingInstance
// 当多个组件都使用当前异步组件时,将使用该组件的Vue实例收集到 factory.owners 中,最后统一执行
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
// ...
if (owner && !isDef(factory.owners)) {
// 将当前 Vue 实例放到 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) => {}
const resolve = once((res: Object | Class<Component>) => {})
const reject = once(reason => {})
const res = factory(resolve, reject)
if (isObject(res)) {
// ...
}
sync = false
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
复制代码
对于普通函数的异步组件,获取当前正在渲染的Vue实例并赋值给owner
。如果factory.owners
为空,定义一个局部变量sync
,执行factory
并传入resolve
和reject
,也就是说执行传入的组件函数
Vue.component('hello-world', function (resolve, reject) {
require(['../components/HelloWorld'], resolve)
})
复制代码
当异步组件加载完成之后会调用resolve
函数,这个之后再说。在加载异步组件期间会继续往下执行,首先将sync
设置为false
,由于普通函数异步组件返回值为undefined
,所以if (isObject(res))
内的逻辑不会执行,直接返回一个undefined
;回到createComponent
方法
const baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
// 这里开始
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
复制代码
此时Ctor
为undefined
,通过createAsyncPlaceholder
方法创建一个注释VNode,表示这里的位置是异步组件的位置
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
}
复制代码
createAsyncPlaceholder
方法就是创建一个注释VNode,并给这个注释VNode添加asyncFactory
属性、asyncMeta
属性
在普通异步组件加载过程中,如果其他组件也使用了这个异步组件也会调用resolveAsyncComponent
方法,并进入下面逻辑
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
// ...
if (owner && !isDef(factory.owners)) {
// ...
}
复制代码
将组件的Vue实例添加到factory.owners
中,由于此时factory.owners
已经有值了,所以返回undefined
,并在createComponent
方法中创建一个注释VNode。
当普通函数异步组件加载完成之后,会调用前面说的resolve
函数
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
复制代码
resolve
函数是once
函数的返回值,once
函数的作用就是保证传入的函数只调用一次,也就是说resolve
和reject
只执行一次
function once (fn) {
var called = false;
return function () {
if (!called) {
called = true;
fn.apply(this, arguments);
}
}
}
复制代码
resolve
函数内调用ensureCtor
函数获取异步组件的构造函数,并将获取的构造函数挂载到factory.resolved
中
function ensureCtor (comp: any, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
return isObject(comp)
? base.extend(comp)
: comp
}
复制代码
普通函数异步组件是通过require
方式加载的,而webpack在编译过程中会实现require
方法,给导出的内容添加一个__esModule
属性,并修改导出内容的Symbol.toStringTag
属性的值。
ensureCtor
函数就是获取导出内容,如果导出内容是一个函数则直接返回,如果是一个对象则调用Vue.extend
方法创建组件的构造函数
回到resolve
函数中,此时变量sync
为false
,会执行forceRender(true)
方法
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
// 如果组件加载成功或加载失败,则传入 true。清空 loading 和 timeout 的定时器
// 如果是 loading状态 则 传入 false
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {}
if (timerTimeout !== null) {}
}
}
复制代码
forceRender
函数内遍历所有使用到该异步组件的组件实例,并调用它们的$forceUpdate
触发这些组件更新。在重新创建VNode过程中,再次调用resolveAsyncComponent
方法,里面有这样一段逻辑
if (isDef(factory.resolved)) {
return factory.resolved
}
复制代码
此时factory.resolved
有值了,值为异步组件的构造函数,所以这里就和创建正常组件VNode流程一样了。当所有VNode都创建完成之后,进入patch过程,通过sameVnode
比对异步组件VNode和注释VNode时,返回false
,重新创建异步组件的DOM树,并删除注释VNode
Promise 异步组件
Vue.component('hello-world', () => import('./components/HelloWorld'))
复制代码
Promise 异步组件的函数中,会返回一个Promise
,所以在resolveAsyncComponent
方法中的处理方式和普通函数异步组件不同
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
}
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
if (owner && !isDef(factory.owners)) {
// 将当前 Vue 实例放到 factory.owners 中
const owners = factory.owners = [owner]
let sync = true
let timerLoading = null
let timerTimeout = null
const forceRender = (renderCompleted: boolean) => {}
const resolve = once((res: Object | Class<Component>) => {})
const reject = once(reason => {})
// 这里返回的是 Promise
const res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {
// ...
}
}
sync = false
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
复制代码
当调用factory
时,会返回一个Promise
对象,所以if (isObject(res))、if (isPromise(res))
都为true
,并且if (isUndef(factory.resolved))
也为true
,所以会给这个res
添加then
方法,并将resolve, reject
传入。最后resolveAsyncComponent
也返回一个undefined
,createComponent
方法会创建一个注释节点。
当组件加载成功之后调用resolve
函数,函数内遍历组件实例并调用$forceUpdate
方法触发更新。
高级异步组件
const LoadingComp = {
template: '<div>loading...</div>'
}
const ErrorComp = {
template: '<div>error...</div>'
}
const AsyncComp = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('../components/HelloWorld'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('hello-world', AsyncComp)
复制代码
高级异步组件可以设置中间状态,比如加载状态、出错状态,这些状态的加载也是在resolveAsyncComponent
中
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
}
// currentRenderingInstance 在 _render 函数中被赋值,指向当前的 Vue 实例
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
// 加载中
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
if (owner && !isDef(factory.owners)) {
// 将当前 Vue 实例放到 factory.owners 中
const owners = factory.owners = [owner]
let sync = true
let timerLoading = null
let timerTimeout = null
const forceRender = (renderCompleted: boolean) => {}
const resolve = once((res: Object | Class<Component>) => {})
const reject = once(reason => {})
const res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
// ...
} else if (isPromise(res.component)) {
// ...
}
}
sync = false
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
复制代码
对于高级异步组件也是先执行 const res = factory(resolve, reject)
加载组件并且返回值是一个对象,所以else if (isPromise(res.component))
为true
进入下面逻辑
else if (isPromise(res.component)) {
res.component.then(resolve, reject)
if (isDef(res.error)) {
factory.errorComp = ensureCtor(res.error, baseCtor)
}
// ...
复制代码
给res.component
添加then
方法,当组件加载完成后执行resolve
函数。如果设置了error
属性,获取error
对应的组件内容,并挂载到factory.errorComp
上。继续执行
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)
}
}
// ...
复制代码
如果有loading
属性,获取loading
对应的组件内容,并挂载到factory.loadingComp
上。如果有delay
属性并且值为0
,将factory.loading
设置为true
,说明要立马渲染loading
组件。否则添加一个延时时间为delay
的定时器,表示delay
毫秒后才会渲染loading
组件;默认是200ms
。继续执行
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)
}
// ...
复制代码
如果设置了timeout
属性,设置一个延时时间为timeout
的定时器,表示超时时间。继续执行
sync = false
return factory.loading
? factory.loadingComp
: factory.resolved
复制代码
上面说过如果设置的delay
为0
,factory.loading
为true
,所以会直接返回loading
组件,而不是创建一个注释VNode
接下来的几种情况
加载超时
加载超时会执行延时时间为timeout
的定时器,清空定时器,并执行reject
函数
reject
函数如下,如果配置了error
组件则将factory.error = true
,并调用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(true)
}
})
复制代码
forceRender
函数就是让组件更新,更新过程重新进入resolveAsyncComponent
方法,返回error
组件
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
// ...
}
复制代码
回到forceRender
,因为传入的参数为true
,所以会执行剩余逻辑
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
复制代码
if
里面的逻辑是清空owners
,如果delay
的定时器不为空则清空这个定时器,如果超时的定时器不为空,则清空这个定时器
触发超时定时器后,组件请求回来了还会不会执行reslove
?
不会再次执行,因为resolve
和reject
是once
的返回值,once
的作用就是保证回调只执行一次
超出loading组件等待时间
如果超出延时时间,并且此时组件还没有加载完成,则将factory.loading
设置为true
,并执行forceRender
方法
// 这个时候传入的 renderCompleted 为 false
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
if (renderCompleted) {}
}
复制代码
forceRender
内触发组件更新,会重新执行resolveAsyncComponent
方法,方法内有如下逻辑,会渲染loading
组件
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
复制代码
组件加载成功
这个就和之前的一样了,就是调用reslove
函数,获取组件的内容并挂载给factory.resolved
上,调用forceRender
函数,触发组件更新,更新过程会重新进入resolveAsyncComponent
方法,由于factory.resolved
有值则直接返回组件内容。组件更新完成后,回到forceRender
内,清空所有定时器和变量的值
总结
这三种组件的更新方式首先将使用到当前异步组件的组件实例收集起来,并实现了加载成功和加载失败的回调(resolve
、reject
);然后执行传入的异步函数;当组件加载完成之后会调用成功的回调resolve
,成功的回调resolve
内遍历收集的所有实例触发它们的$forceUpdate
方法重新渲染。
在等待过程中,对于高级异步组件来说可以先使用loading
节点,而其他两种则都是注释节点;如果加载失败,高级异步组件会渲染一个错误节点,而其他两种一直是注释节点。