在开发过程中,随着工程越来越大,为了减少首屏js包的体积,可以使用异步组件的方式。Vue.js 实现了三种方式:工厂函数、Promise、高级异步组件。
工厂函数
Vue.component('HelloWorld', function (res, rej) {
require(['./components/HelloWorld'], function (data) {
res(data)
})
})
以上为使用工厂函数的方式加载异步组件的示例。接下来我们探讨整个过程是如何实现的,在_createElement中通过resolveAsset()获取到 Ctor(上篇组件注册中有详解,即我们定义的工厂函数),因此在createComponent中,发现 Ctor 是一个函数,不会经过Vue.extend()处理,因此Ctor.cid为 undefined,因此会执行resolveAsyncComponent(asyncFactory, baseCtor),接下来看这个函数做了什么。
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// Vue
const baseCtor = context.$options._base
// Ctor is a function rather than an object
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
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)
}
}
// return a placeholder vnode
}
在resolveAsyncComponent(asyncFactory, baseCtor)中,定义变量 owner,赋值为当前正在渲染的实例(vm)。factory 即为我们定义的工厂函数,最初factory.owners为 undefined,所以进入下面逻辑,定义forceRender、resolve、reject函数,异步去执行factory(resolve, reject),resolveAsyncComponent函数执行完毕,返回undefined。回到createComponent,此时 Ctor 为 undefined,createAsyncPlaceholder执行,返回一个空节点(实际为注释节点,但保留了渲染真实 node 需要所有信息)。
在factory(resolve, reject)中文件引入成功之后执行 resolve 函数,对组件的构造器做缓存之后执行forceRender,强制重新渲染。依照如下流程:$forceUpdate -> vm._watcher.update() -> vm._update(vm._render(), hydrating),最终会重新回到resolveAsyncComponent,此时判断factory.owners存在,直接返回,赋值给Ctor(此时产生了真实的组件构造器),获取到构造器之后就与同步加载的组件没有区别了。
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
if (isDef(factory.resolved)) {
return factory.resolved
}
const owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
// already pending
factory.owners.push(owner)
}
if (owner && !isDef(factory.owners)) {
const owners = factory.owners = [owner]
let sync = true
const forceRender = (renderCompleted: boolean) => {
for (let i = 0, l = owners.length; i < l; i++) {
(owners[i]: any).$forceUpdate()
}
}
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
const res = factory(resolve, reject)
sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
Promise
以下是一个使用Promise的示例。
Vue.component('HelloWorld', () => import('./components/HelloWorld.vue'))
由于工厂函数执行时import()会返回一个 Promise 对象,进入下面的 if 判断,调用 then 方法,其余流程与工厂函数的方式相同。
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>
): Class<Component> | void {
// ...
const res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
}
}
}
高级异步组件
以下是一个高级异步组件的示例。
const LoadiComp = { template : '<div>Loading...</div>' }
const ErrorComp = { template : '<div>Error Occurred!</div>' }
const AsyncComp = () => ({
component: import('./components/HelloWorld.vue'),
loading: LoadiComp,
error: ErrorComp,
delay: 300,
timeout: 1000
})
Vue.component('HelloWorld', AsyncComp)
在前面两种的基础上,高级异步组件又增加了loading组件、error组件、delay、timeout等。实现方式还是基于resolveAsyncComponent,工厂函数返回一个对象,此对象不是 Promise,但对象的 component 属性是一个 Promise,在此调用 then 方法。同时存储 error组件和 loading组件(如果配置中有的话),再通过 forceRender 重新渲染,等等。此处不做详细讲解了,如果还有疑问可以通过断点调试的方式,相信一定会恍然大悟。
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 (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
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
}
}
}
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})
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)) {
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
}
}
总的来说,异步组件的实现通常是2次渲染,先渲染成注释节点,组件加载成功后再通过forceRender重新渲染,这是异步组件的核心所在。