webpack
遇到异步组件,会将其从主脚本中分离,减少脚本体积,加快首屏加载时间。当遇到场景需要使用该组件时,才会去加载组件脚本。
6.1.2 工厂函数
Vue
中允许用户通过工厂函数的形式定义组件,这个工厂函数会异步解析组件定义,组件需要渲染的时候才会触发该工厂函数,加载结果会进行缓存,以供下一次调用组件时使用。具体使用:
有了上一节组件注册的基础,我们来分析异步组件的实现逻辑。简单回忆一下上一节的流程,实例的挂载流程分为根据渲染函数创建Vnode
和根据Vnode
产生真实节点的过程。期间创建Vnode
过程,如果遇到子的占位符节点会调用creatComponent
,这里会为子组件做选项合并和钩子挂载的操作,并创建一个以vue-component-
为标记的子Vnode
,而异步组件的处理逻辑也是在这个阶段处理。
// 创建子组件过程
function createComponent (
Ctor, // 子类构造器
data,
context, // vm实例
children, // 子节点
tag // 子组件占位符
) {
···
// 针对局部注册组件创建子类构造器
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// 异步组件分支
var asyncFactory;
if (isUndef(Ctor.cid)) {
// 异步工厂函数
asyncFactory = Ctor;
// 创建异步组件函数
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
···
// 创建子组件vnode
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
return vnode
}
工厂函数的用法使得Vue.component(name, options)
的第二个参数不是一个对象,因此不论是全局注册还是局部注册,都不会执行Vue.extend
生成一个子组件的构造器,所以Ctor.cid
不会存在,代码会进入异步组件的分支。
异步组件分支的核心是resolveAsyncComponent
,它的处理逻辑分支众多,我们先关心工厂函数处理部分。
function resolveAsyncComponent (
factory,
baseCtor
) {
if (!isDef(factory.owners)) {
// 异步请求成功处理
var resolve = function() {}
// 异步请求失败处理
var reject = function() {}
var res = factory(resolve, reject);
// resolved 同步返回
return factory.loading
? factory.loadingComp
: factory.resolved
}
}
如果经常使用promise
进行开发,我们很容易发现,这部分代码像极了promsie
原理内部的实现,针对异步组件工厂函数的写法,大致可以总结出以下三个步骤:
- 定义异步请求成功的函数处理,定义异步请求失败的函数处理;
- 执行组件定义的工厂函数;
- 同步返回请求成功的函数处理。
resolve, reject
的实现,都是once
方法执行的结果,所以我们先关注一下高级函数once
的原理。为了防止当多个地方调用异步组件时,resolve,reject
不会重复执行,once
函数保证了函数在代码只执行一次。也就是说,once
缓存了已经请求过的异步组件
// once函数保证了这个调用函数只在系统中调用一次
function once (fn) {
// 利用闭包特性将called作为标志位
return function () {
// 调用过则不再调用
if (!called) {
called = true;
fn.apply(this, arguments);
}
}
}
// 成功处理
var resolve = once(function (res) {
// 转成组件构造器,并将其缓存到resolved属性中。
factory.resolved = ensureCtor(res, baseCtor);
if (!sync) {
//强制更新渲染视图
forceRender(true);
} else {
owners.length = 0;
}
});
// 失败处理
var reject = once(function (reason) {
warn(
"Failed to resolve async component: " + (String(factory)) +
(reason ? ("\nReason: " + reason) : '')
);
if (isDef(factory.errorComp)) {
factory.error = true;
forceRender(true);
}
});
异步组件加载完毕,会调用resolve
定义的方法,方法会通过ensureCtor
将加载完成的组件转换为组件构造器,并存储在resolved
属性中,其中 ensureCtor
的定义为:
组件构造器创建完毕,会进行一次视图的重新渲染,由于Vue
是数据驱动视图渲染的,而组件在加载到完毕的过程中,并没有数据发生变化,因此需要手动强制更新视图。forceRender
函数的内部会拿到每个调用异步组件的实例,执行原型上的$forceUpdate
方法,这部分的知识等到响应式系统时介绍。
异步组件加载失败后,会调用reject
定义的方法,方法会提示并标记错误,最后同样会强制更新视图。
回到异步组件创建的流程,执行异步过程会同步为加载中的异步组件创建一个注释节点Vnode
function createComponent (){
···
// 创建异步组件函数
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
// 创建注释节点
return createAsyncPlaceholder(asyncFactory,data,context,children,tag)
}
}
createAsyncPlaceholder
的定义也很简单,其中createEmptyVNode
之前有介绍过,是创建一个注释节点vnode
,而asyncFactory,asyncMeta
都是用来标注该节点为异步组件的临时节点和相关属性。
// 创建注释Vnode
function createAsyncPlaceholder (factory,data,context,children,tag) {
var node = createEmptyVNode();
node.asyncFactory = factory;
return node
}
执行forceRender
触发组件的重新渲染过程时,又会再次调用resolveAsyncComponent
,这时返回值Ctor
不再为 undefined
了,因此会正常走组件的render,patch
过程。这时,旧的注释节点也会被取代。
6.1.4 Promise异步组件
异步组件的第二种写法是在工厂函数中返回一个promise
对象,我们知道import
是es6
引入模块加载的用法,但是import
是一个静态加载的方法,它会优先模块内的其他语句执行。因此引入了import()
,import()
是一个运行时加载模块的方法,可以用来类比require()
方法,区别在于前者是一个异步方法,后者是同步的,且import()
会返回一个promise
对象。
具体用法:
Vue.component('asyncComponent', () => import('./test.vue'))
var res = factory(resolve, reject);
// res是返回的promise
if (isObject(res)) {
if (isPromise(res)) {
if (isUndef(factory.resolved)) {
// 核心处理
}
}
}
其中promise
对象的判断最简单的是判断是否有then
和catch
方法:
为了在操作上更加灵活,比如使用loading
组件处理组件加载时间过长的等待问题,使用error
组件处理加载组件失败的错误提示等,Vue
在2.3.0+版本新增了返回对象形式的异步组件格式,对象中可以定义需要加载的组件component
,加载中显示的组件loading
,加载失败的组件error
,以及各种延时超时设置,源码同样进入异步组件分支。
Vue.component('asyncComponent', () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
}))
异步组件函数执行后返回一个对象,并且对象的component
执行会返回一个promise
对象,因此进入高级异步组件处理分支。
if (isObject(res)) {
if (isPromise(res)) {}
// 返回对象,且res.component返回一个promise对象,进入分支
// 高级异步组件处理分支
else if (isPromise(res.component)) {
// 和promise异步组件处理方式相同
res.component.then(resolve, reject);
···
}
}
异步组件会等待响应成功失败的结果,与此同时,代码继续同步执行。高级选项设置中如果设置了error
和loading
组件,会同时创建两个子类的构造器,
if (isDef(res.error)) {
// 异步错误时组件的处理,创建错误组件的子类构造器,并赋值给errorComp
factory.errorComp = ensureCtor(res.error, baseCtor);
}
if (isDef(res.loading)) {
// 异步加载时组件的处理,创建错误组件的子类构造器,并赋值给errorComp
factory.loadingComp = ensureCtor(res.loading, baseCtor);
}
如果存在delay
属性,则通过settimeout
设置loading
组件显示的延迟时间。factory.loading
属性用来标注是否是显示loading
组件。
if (res.delay === 0) {
factory.loading = true;
} else {
// 超过时间会成功加载,则执行失败结果
setTimeout(function () {
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true;
forceRender(false);
}
}, res.delay || 200);
}
如果在timeout
时间内,异步组件还未执行resolve
的成功结果,即resolve
没有赋值,则进行reject
失败处理。
接下来依然是渲染注释节点或者渲染loading
组件,等待异步处理结果,根据处理结果重新渲染视图节点,相似过程不再阐述。
6.1.6 wepack异步组件用法
webpack
作为Vue
应用构建工具的标配,我们需要知道Vue
如何结合webpack
进行异步组件的代码分离,并且需要关注分离后的文件名,这个名字在webpack
中称为chunkName
。webpack
为异步组件的加载提供了两种写法。
require.ensure
:它是webpack
传统提供给异步组件的写法,在编译时,webpack
会静态地解析代码中的require.ensure()
,同时将模块添加到一个分开的chunk
中,其中函数的第三个参数为分离代码块的名字。修改后的代码写法如下:
import(/* webpackChunkName: "asyncComponent" */, component)
: 有了es6
,import
的写法是现今官方最推荐的做法,其中通过注释webpackChunkName
来指定分离后组件模块的命名。修改后的写法如下: