前言
通过这篇文章可以了解如下内容
- Vue 有几种异步组件
- 这些异步组件的实现原理
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)
}
// ...
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内,清空所有定时器和变量的值
总结
Vue 有几种异步组件
Vue中总共有3种异步组件,分别是普通函数异步组件、Promise异步组件和高级异步组件
这些异步组件的实现原理
首先将使用到当前异步组件的组件实例收集起来,并实现了加载成功和加载失败的回调(resolve、reject);然后执行传入的异步函数;当组件加载完成之后会调用成功的回调resolve,成功的回调resolve内根据导出内容创建异步组件的Vue实例;缓存这个实例。然后遍历收集的所有实例触发它们的$forceUpdate方法重新渲染。创建新的VNode时,由于异步组件已经缓存了Vue实例,所以会创建该组件的占位符VNode。
在等待过程中,对于高级异步组件来说可以先使用loading节点,而其他两种则都是注释节点;如果加载失败,高级异步组件会渲染一个错误节点,而其他两种一直是注释节点。
简单点说就是异步组件在加载时会被渲染成一个注释节点或loading节点并收集依赖该异步组件的组件实例;加载完成后创建异步组件的Vue实例并对收集的组件实例调用$forceUpdate方法触发视图更新