组件化-异步组件
在开发中,我们为了减少打包体积或为了加快首屏的记载速度,会把代码块进行分割,只有在使用的时候才加载,为此 Vue
提供了异步组件,它允许我们用工厂函数的方式来定义组件,这个工厂函数会异步解析我们定义的组件定义,只有在组件需要被渲染的时候才会触发该函数。
下面是异步组件的几种使用方式(官网:异步组件):
// 一:普通函数异步组件
Vue.component("async-webpack-example", function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(["./my-async-component"], resolve);
});
// 二:Promise 异步组件
Vue.component(
"async-webpack-example",
// 这个动态导入会返回一个 `Promise` 对象。
() => import("./my-async-component")
);
// 三:高级异步组件
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import("./MyComponent.vue"),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000,
});
Vue.component("async-example", AsyncComp);
复制代码
我们先来看一下异步组件的大概流程,然后再分析三种方式的差异在哪里。
在上一节我们分析了 12.组件化-组件注册 的逻辑,由于组件的定义不是对象,所以不会执行 Vue.extend
的逻辑把它变成一个组件的构造函数,但是它仍然可以执行到 createComponent
函数,createComponent
定义在 src/core/vdom/create-component/js
中:
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return;
}
const baseCtor = context.$options._base;
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// ...
// async component
let asyncFactory;
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);
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);
}
}
}
复制代码
由于我们传入的 Ctor
参数是一个 Function
,所以并不是对象,Ctor
上面也没有 id
,所以执行 Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
,resolveAsyncComponent
定义在 src/core/vdom/helpers/resolve-async-component.js
中:
export function resolveAsyncComponent(
factory: Function,
baseCtor: Class<Component>,
context: Component
): Class<Component> | void {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp;
}
if (isDef(factory.resolved)) {
return factory.resolved;
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp;
}
if (isDef(factory.contexts)) {
// already pending
factory.contexts.push(context);
} else {
const contexts = (factory.contexts = [context]);
let sync = true;
const forceRender = () => {
for (let i = 0, l = contexts.length; i < l; i++) {
contexts[i].$forceUpdate();
}
};
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();
}
});
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();
}
});
const res = factory(resolve, reject);
if (isObject(res)) {
if (typeof res.then === "function") {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject);
}
} else if (
isDef(res.component) &&
typeof res.component.then === "function"
) {
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 {
setTimeout(() => {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true;
forceRender();
}
}, res.delay || 200);
}
}
if (isDef(res.timeout)) {
setTimeout(() => {
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;
}
}
复制代码
resolveAsyncComponent
函数逻辑复杂的原因就是它处理了我们开头讲的注册异步组件的三种方式,下面我们分别来看三种方式的不同逻辑。
普通函数异步组件
// 一:普通函数异步组件
Vue.component("async-webpack-example", function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(["./my-async-component"], resolve);
});
复制代码
首先传进来的 factory
是一个 Function
,所以前面的几个 if
可以忽略,对于 factory.contexts
的判断,是考虑到多个地方同时初始化一个异步组件,那么它的实际加载应该只有一次。然后定义了 forceRender
、resolve
和 reject
函数,注意到 resolve
和 reject
函数用 once
包了一层,once
的作用就是利用闭包和标识符保证函数只会执行一次,即为 单例模式。
接下来执行 const res = factory(resolve, reject)
,在这里就是执行我们定义的组件工厂函数,同时把 resolve
和 reject
传入,组件的工厂函数会发送请求加载我们的异步组件,拿到组件文件导出的 Object
后执行 resolve(res)
,即执行:
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();
}
});
复制代码
然后执行 factory.resolved = ensureCtor(res, baseCtor)
function ensureCtor(comp: any, base) {
if (comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === "Module")) {
comp = comp.default;
}
return isObject(comp) ? base.extend(comp) : comp;
}
复制代码
这个函数的目的是为了能保证找到组件定义的对象,如果是对象则调用 base.extend
转换为 Vue
实例的子构造函数。
resolve
函数最后执行 forceRender
函数,他会遍历 contexts
,拿到每一个 调用 异步组件的实例 vm
,执行 vm.$forceUpdate()
Vue.prototype.$forceUpdate = function () {
const vm: Component = this;
if (vm._watcher) {
vm._watcher.update();
}
};
复制代码
$forceUpdate
的逻辑非常简单,就是调用渲染 watcher
的 update
方法,让渲染 watcher
对应的回调函数执行,也就是触发了组件的重新渲染。之所以这么做是因为 Vue
通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate
可以强制组件重新渲染一次。
Promise 异步组件
在 webpack 2+
和 ES2015
的支持下,我们可以使用异步加载的语法糖?,导入返回 Promise
的组件
// 二:Promise 异步组件
Vue.component(
"async-webpack-example",
// 这个动态导入会返回一个 `Promise` 对象。
() => import("./my-async-component")
);
复制代码
当执行完 const res = factory(resolve, reject)
后,会返回一个 Promise 对象
,进入 if
条件:
if (isUndef(factory.resolved)) {
res.then(resolve, reject);
}
复制代码
当异步加载成功之后,执行 resolve
,否则执行 reject
,又回到了上面普通异步组件的逻辑。
高级异步组件
由于异步加载组件有一定的延迟,比如网络波动或者其他问题,所以 Vue 2.3+
支持一种高级异步组件的方式,里面可以配置 error
和 loading
,并在合适时机会渲染他们,提高用户体验。
// 三:高级异步组件
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import("./MyComponent.vue"),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000,
});
Vue.component("async-example", AsyncComp);
复制代码
高级异步组件和普通异步组件的初始化逻辑是一样的,都是执行 resolveAsyncComponet
,当执行完 const res = factory(resolve, reject)
后,返回值就是定义的对象,所以走到了下面的逻辑:
if (typeof res.then === "function") {
} else if (isDef(res.component) && typeof res.component.then === "function") {
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 {
setTimeout(() => {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true;
forceRender();
}
}, res.delay || 200);
}
}
if (isDef(res.timeout)) {
setTimeout(() => {
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== "production"
? `timeout (${res.timeout}ms)`
: null
);
}
}, res.timeout);
}
}
复制代码
首先执行 res.component.then(resolve, reject)
,当加载成功后执行 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 {
setTimeout(() => {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true;
forceRender();
}
}, res.delay || 200);
}
}
if (isDef(res.timeout)) {
setTimeout(() => {
if (isUndef(factory.resolved)) {
reject(
process.env.NODE_ENV !== "production"
? `timeout (${res.timeout}ms)`
: null
);
}
}, res.timeout);
}
复制代码
根据 res
的值内容,分别设置 factory.errorComp
和 factor.loadingComp
。在设置 loading
组件的时候设置 delay
。如果定义了 timeout
,则再超时时间之后判断有没有加载成功,如果没加载成功的话调用 reject
。
在 resolveAsyncComponent
最后还有一段代码:
sync = false;
return factory.loading ? factory.loadingComp : factory.resolved;
复制代码
如果 delay
为 0
,则直接渲染 loading
组件,否则延时 delay
执行 forceRender
,那么又再一次执行到 resolveAsyncComponent
。
最后看一下加载组件的边界处理:
异步组件加载失败
会执行 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)) {
factory.error = true;
forceRender();
}
});
复制代码
这时会把 factory.error
设置为 true
,并执行 forceRender
,重新执行 resolveAsyncComponent
:
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp;
}
复制代码
这个时候就会直接渲染 errorComp
组件。
异步组件加载成功
会执行 resolve
函数:
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor);
if (!sync) {
forceRender();
}
});
复制代码
首先把结果缓存到 factory.resolved
中,因为这时 sync
为 false
,所以执行 forceRender
,重新执行 resolveAsyncComponent
,渲染加载成功的组件。
异步组件加载中
如果异步组件正在加载中,则:
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp;
}
复制代码
直接返回 loadingComp
,渲染 loading
组件。
异步组件加载超时
如果超时,则走 reject
逻辑,渲染 error
组件。
异步组件 patch
让我们回到本文一开始的 createComponent
函数:
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
}
复制代码
如果不是用高级异步组件 0 delay
创建的 loading
组件,则返回的是 undefined
,接着会调用 createAsyncPlaceholder
来创建一个占位符节点。
createAsyncPlaceholder
定义在 src/core/vdom/helpers/resolve-async-components.js
中:
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
,然后把 asyncFactory
和 asyncMeta
赋值给 vnode
。当执行 forceRender
时,触发组件重新渲染,那么会再次执行 resolveAsyncComponent
。这时就会根据情况返回 loading
、error
或者加载成功的组件,返回值不为 undefined
,因此就走正常的组件 render
、patch
过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode
的,下一章我们会分析组件更新的 patch
过程。
总结
在本节中,我们知道了异步组件的三种实现方式,并且高级异步组件的实现非常有意思。它实现了 loading
、resolve
、reject
、timeout
四种状态。
异步组件的本质是二次渲染,都是第一次渲染生成一个占位符节点,然后组件获取成功之后,再通过 forceRender
强制重新渲染,但 0 delay
的高级异步组件是个例外,他第一次是直接渲染 loading
组件。
参考:Vue.js 技术揭秘