Vue大多用在单页面应用中,一个页面只有一个
<div id="app"></div>承载所有节点,因此复杂项目可能会出现首屏加载白屏等问题,Vue异步组件就很好的处理了这问题。。
一、异步组件渲染主流程
1、Vue.component注册
首先执行Vue.component进行组件的注册:
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
}
}
})
其中isPlainObject(definition)为false,最终执行到this.options[type + 's'][id] = definition,没有像同步组件一样对异步组件处理成构造函数,此时的definition还是原始数据。
2、createComponent函数
组件实例化vNode的详细过程请移步juejin.cn/post/712909…
这里只记录异步组件不一样的地方,在异步组件中,并没有执行逻辑 Ctor = baseCtor.extend(Ctor),所以Ctor.cid未定义,进入以下逻辑:
// 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
)
}
}
asyncFactory就是当前的Ctor,baseCtor就是Vue,执行解析异步组件的方法Ctor = resolveAsyncComponent(asyncFactory, baseCtor):
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) {
// already pending
factory.owners.push(owner)
}
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
;(owner: any).$on('hook:destroyed', () => remove(owners, owner))
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)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
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)
}
})
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
}
}
这个过程中,有以下主要流程:
(1)定义 owners
在_render刚开始执行的时候先定义currentRenderingInstance = vm,然后再当前方法中var owner = currentRenderingInstance与var owners = factory.owners = [owner]共同决定了,factory.owners就是一个vm实例。
(2)定义resolve
resolve中主要实现了函数只执行一次,将对象转换成组件构造函数和进行视图的强制渲染。
/**
* 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,一个函数只执行一次的once是通过闭包中控制called变量来实现的。
function ensureCtor (comp: any, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
return isObject(comp)
? base.extend(comp)
: comp
}
确保异步执行结束后,返回的结果如果是普通对象,则将其通过Vue上的方法转换成构造函数。
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
}
}
}
该方法是等异步执行结束后,owners执行$forceUpdate()函数:
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
会执行Watcher实例的update方法,进行视图的重新渲染。
(3)定义reject
同样用once进行包裹,如果定义了factory.errorComp还会执行factory.error = true;forceRender(true)。
(4)执行factory
当前方法const res = factory(resolve, reject)如果异步执行,会推到异步队列中等待同步执行完毕后执行其中的resolve函数;如果是同步函数,则会返回res。
(5)判断res是否存在
如果const res = factory(resolve, reject)中返回res,并且res.component是Promise,就会执行res.component.then(resolve, reject),在.then方法中执行resolve回调。在整个过程中还根据res中的属性,还做了以下几件事:
- 如果
res.error存在,通过factory.errorComp = ensureCtor(res.error, baseCtor)获取错误时的组件构造函数 - 如果
res.loading存在,通过factory.loadingComp = ensureCtor(res.loading, baseCtor)获取加载时的构造函数。如果延时res.delay === 0则factory.loading = true,并且在解析异步组件结束时return factory.loading ? factory.loadingComp : factory.resolved,直接返回factory.loadingComp作为组件构造函数,在后续的渲染中直接渲染加载组件;如果有延时,则定义一个定时器,在定时器结束时组件既没有失败也没有成功,则进行加载组件的渲染。 - 如果
res.timeout存在,则定义一个定时器,在时间res.timeout后factory.resolved依然不存在,说明组件没有完成引入,则执行reject,进而执行失败组件的渲染。
(6) 返回组件构造函数
sync = false将异步变量置为false,并通过return factory.loading ? factory.loadingComp : factory.resolved返回需要渲染的组件构造函数。
二、例子
异步组件的引入有vue.js官网推荐的如下方式
1、require引入
Vue.component("async-Component-1", function(resolve, reject) {
setTimeout(() => {
require(["./components/asyn-component-1.vue"], function(res) {
resolve(res);
});
}, 1000);
});
在当前例子中会等待引入async-Component-1后执行成功后视图的渲染,ensureCtor将组件对象转换成构造函数,然后forceRender(true)强制渲染视图。
2、import引入
把webpack 2 和 ES2015 语法加在一起,可以使用动态导入
Vue.component("async-Component-2", () =>
import("./components/asyn-component-2.vue")
);
3、处理加载状态
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
Vue.component('asyncComponent', AsyncComponent)
这个例子中失败时会渲染组件ErrorComponent,否则,怎么判定先执行LoadingComponent还是执行import('./MyComponent.vue')?
如果,import('./MyComponent.vue')先引入加载完成,则先执行res.component.then(resolve, reject)中的resolve,进而渲染成功后的组件;如果
timerLoading = setTimeout(() => {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
先执行,那么会先通过forceRender(false)去进行加载组件的渲染,这里的false表示不会进行当前关联实例owners的清空和当前timerLoading的终止,等待res.component.then(resolve, reject)执行resolve的逻辑。
这里主要的是,Promise异步res.component.then(resolve, reject)和定时器timerLoading争取执行权,谁跑到前面先执行谁。
小结
异步组件是首屏加载优化和骨架屏实现的方案之一,同时,异步组件的思路也可以通过权限管理来控制功能组件的渲染的权限。