异步组件的官方文档点击这里。
借助于异步组件,我们可以将 Vue 项目按照组件分割成一些小的代码块,并且让这些代码块在前端需要时才从服务器进行加载。这种优化措施在大型应用中是很有必要的,可以大大缩短首次加载的时间。
在这里,建议读者先将 Vue 官网中的异步组件部分复习一遍,了解异步组件的写法。
1,异步组件的加载时机
首先说明一下异步组件触发加载的时间,异步组件的加载是在执行 render 函数创建 vnode 的过程中,如果判断出当前创建 vnode 的组件是异步组件的话,则会执行 resolveAsyncComponent 方法到服务器请求该异步组件的资源,相关源码如下所示:
// 创建组件的 vnode
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | void {
......
// 异步组件的处理
let asyncFactory
// 下面的 if 代码块是处理异步组件的逻辑
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
// 返回一个占位用的 VNode,将会被渲染成一个注释节点。
// 但是这个 VNode 保留了渲染这个异步组件所需的所有信息数据
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
... 本地组件的处理逻辑 ...
}
createComponent 方法用于创建组件对应的 vnode,当构造方法 Ctor 没有 cid 属性的时候,说明当前的组件是一个异步组件,此时需要借助 resolveAsyncComponent 方法去服务器请求该异步组件的资源,resolveAsyncComponent 方法的返回值是异步组件的构造函数,不过由于异步请求需要时间,所以这里同步代码获取的 Ctor 返回值有可能是 undefined,当 Ctor 是 undefined 的时候,Vue 的做法是返回一个注释站位用的 vnode。
当异步组件经过首次加载后,异步组件的资源会被缓存起来,此时,如果当前的异步组件再次被使用的话,上面的 resolveAsyncComponent 方法则会直接返回已经请求成功的异步组件资源,此时 resolveAsyncComponent 方法的返回值 Ctor 就不是 undefined 了,代码的执行逻辑会进入下面的本地组件的处理逻辑。
2,resolveAsyncComponent 方法的源码解析
resolveAsyncComponent 方法定义在 src/core/vdom/helpers/resolve-async-component.js 文件中,我在源码中写了大量的注释和个人理解,边看源码边看注释,即可较为轻松的理解,源码如下所示:
// 异步组件实现的本质是 2 次渲染,先渲染成注释节点,当组件加载成功后,再通过 forceRender 重新渲染
// 异步组件的写法
// 1:处理异步组件(工厂函数)
// 最终返回 该组件的构造函数
// Vue.component('HelloWorld', function (resolve, reject) {
// // 这个特殊的 require 语法告诉 webpack
// // 自动将编译后的代码分割成不同的块
// // 下面的代码其实就是发送 ajax 请求,到后端获取这个组件的数据,
// require(['./components/HelloWorld'], function (res) {
// // 这个方法是发送 ajax 的回调函数,它是异步的,会在 ajax 请求完成后进行执行
// // 它的执行时机要晚于同步代码
// resolve(res)
// })
// })
// 2:处理异步组件(工厂函数 + Promise)
// Vue.component('HelloWorld', () => import('./components/HelloWorld.vue'))
// 3:高级异步组件
// const AsyncComp = () => ({
// // 需要加载的组件,应当是一个 Promise
// component: import('./components/HelloWorld.vue'),
// // 加载中应当渲染的组件
// loading: LoadingComp,
// // 出错时渲染的组件
// error: ErrorComp,
// // 渲染加载中组件前的等待时间。默认:200ms。
// delay: 200,
// // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
// timeout: 1000
// })
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>,
context: Component
): Class<Component> | void {
// resolveAsyncComponent 函数会被多次触发执行,第一次执行,发送请求,获取异步组件的信息
// 无论异步组件的信息是否正常获取,都会将相关信息赋值到 factory 上面,这里的相关信息包括
// error、resolved、loading 等表示异步组件获取状态的变量,然后执行 forceRender 方法
// 重新渲染,这会再次进入 resolveAsyncComponent 函数,此时就可以根据 error、resolved、loading
// 等数据判断异步组件的加载状态,返回对应的组件信息
//
// 如果 factory.error 变量为 true 的话,说明异步组件加载失败了,此时需要判断 factory.errorComp
// 有没有定义,如果定义了的话,则返回这个异步组件加载失败时应该显示的 error 组件
if (isTrue(factory.error) && isDef(factory.errorComp)) {
// 返回 error 组件
return factory.errorComp
}
// 和上面同理,判断 factory.resolved 是否被定义,如果已经被定义的话,说明当前的异步组件加载成功
// 此时返回这个异步组件的定义即可
if (isDef(factory.resolved)) {
return factory.resolved
}
// 和上面同理,异步组件的加载还有一个加载中的状态,并且可以定义对应的加载中组件,当异步组件正在加载中
// 的时候,会显示这个加载中组件,源码实现就在这个地方
//
// 如果 factory.loading 为 true 并且 factory.loadingComp 被定义了的话,
// 则返回加载中组件
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
// 当第一次执行到这时,此时是当前的异步组件第一次被使用,factory.contexts 肯定没有被定义,
// 代码会进入 else 的逻辑,在 else 的逻辑中,factory.contexts 会被定义,这个 contexts
// 是一个数组,数组中存储使用了当前异步组件的 Vue 实例
//
// 下次执行到这时,说明当前的异步组件已经被使用了,factory.contexts 已经被定义,此时将当前的 Vue 实例
// push 到 factory.contexts 数组中即可
//
// 那么这个 factory.contexts 数组有什么用呢?其实这个数组用于存储当前这个异步组件在加载中的时候,使用了
// 这个异步组件的 Vue 实例,也就是组件,当这个异步组件加载成功或者失败时,可以触发 contexts 数组中所有
// Vue 实例的 $forceUpdate 方法,强制这些使用了当前异步组件的组件重新渲染,进而渲染出这个已经加载完成了
// 的异步组件。
if (isDef(factory.contexts)) {
// 将使用了当前异步组件的 Vue 实例 push 到 factory.contexts 数组中
factory.contexts.push(context)
} else {
// 当前的异步组件第一次被使用时,代码会执行到这,此时需要初始化 factory.contexts
// 初始化时的数据是 [context]
const contexts = factory.contexts = [context]
let sync = true
// 创建一个工具方法 forceRender,它的作用是遍历 factory.contexts 数组中的 Vue 实例
// 执行这个 Vue 实例的 $forceUpdate 方法,强制这些组件进行重新渲染
const forceRender = () => {
for (let i = 0, l = contexts.length; i < l; i++) {
contexts[i].$forceUpdate()
}
}
// 创建异步组件工厂函数的 resolve 参数,是一个函数类型
const resolve = once((res: Object | Class<Component>) => {
// 这里的 res 是请求获取到的异步组件对象,通过 ensureCtor 可以创建出对应的组件构造函数
// 内部借助了 extend 方法
// 将该异步组件的构造函数保存在 factory.resolved 上
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
// 异步组件已经通过 ajax 请求从后端获取到了,所以在这里需要对组件重新渲染
// 将异步组件渲染到页面上
forceRender()
}
})
// 创建异步组件工厂函数的 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)) {
// 如果定义了 errorComp 组件的话,在这里将 factory.error 设置为 true
// 并强制组件重新渲染,当组件重新渲染时,在上面的代码中,会直接返回 errorComp
factory.error = true
forceRender()
}
})
// 执行组件的工厂函数
// 在组件的工厂函数中会执行这个组件的异步加载,通过发送 ajax 请求,
// 获取组件的数据后,将组件的数据当做参数执行 resolve 方法,resolve 方法会进行组件的重新加载
const res = factory(resolve, reject)
if (isObject(res)) {
// 下面的代码块是用于处理 Promise 情况的
// 如果我们:Vue.component() 的写法是返回一个 Promise 的话,那么上面 factory 方法的返回值就是一个 Promise
if (typeof res.then === 'function') {
// () => import('./my-async-component')
if (isUndef(factory.resolved)) {
// 将 resolve 和 reject 回调函数注册到 Promise.then() 中
// 这样当 ajax 请求完成,这个 Promise 就是 resolved 的状态,
// 然后就会执行 resolve 这个回调函数,接下来的逻辑和上面的工厂函数就一样了。
res.then(resolve, reject)
}
} else if (isDef(res.component) && typeof res.component.then === 'function') {
// 下面的代码块是针对 高级异步组件 的情况,此时 res 是一个对象,并且 res.component 是一个 Promise
// 注册 resolve 和 reject 回调函数
res.component.then(resolve, reject)
// 处理 高级异步组件 中的 error
if (isDef(res.error)) {
// 创建 error 组件的构造函数,并保存在 errorComp 属性中
factory.errorComp = ensureCtor(res.error, baseCtor)
}
// 处理 高级异步组件 中的 loading
if (isDef(res.loading)) {
// 创建 loading 组件的构造函数,并保存在 loadingComp 属性中
factory.loadingComp = ensureCtor(res.loading, baseCtor)
if (res.delay === 0) {
// 如果 delay 为 0 的话,说明要立即进行加载中的状态
factory.loading = true
} else {
// 如果 delay 不等于 0 的话,则需要 delay 之后再进行 loading 的处理
// 此处使用 setTimeout(() => {}, res.delay || 200)
setTimeout(() => {
// delay 毫秒之后,如果不是 resolved 和 error 的状态的话,说明当前是 loading 状态
if (isUndef(factory.resolved) && isUndef(factory.error)) {
// 将加载中的标志为 true,然后重新渲染视图,渲染出加载组件
factory.loading = true
forceRender()
}
}, res.delay || 200)
}
}
// 处理 高级异步组件 中的 timeout
// timeout 参数表示:timeout 毫秒之后,如果这一个异步组件还不是 resolved 状态的话,
// 就将组件设为 error 状态,并重新渲染,渲染出 error 组件,借助 setTimeout 和 reject 方法实现功能
if (isDef(res.timeout)) {
setTimeout(() => {
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== 'production'
? `timeout (${res.timeout}ms)`
: null
)
}
}, res.timeout)
}
}
}
sync = false
// 如果 factory.loading 为 true 的话,说明异步组件还在加载中,此时返回 loadingComp
// 如果不为 true 的话,说明异步组件加载完成,返回 resolved 异步组件即可
return factory.loading
? factory.loadingComp
: factory.resolved
}
}