之前在回顾自己写的一个后台管理项目时,发现用到了Vue的异步组件。而之前恰好在研究vue的源码,顺便分析下异步组件的加载和执行过程。
异步组件是什么?
异步,是相对于同步而言的。我们在使用Vue时,使用到的组件大多为同步组件。在vue实例第一次执行渲染的过程中,已经生成了组件构造器。而异步组件则是在用到该组件时,异步通过请求去拉取对应组件的js(这里需要和webpack的import相结合)。当对应的js加载完成后,获取到异步组件的配置项,从而创建组件构造器。再通过forceRender,强制依赖它的vm实例重新触发渲染函数。
为什么当时项目要选用异步组件呢?
因为对于后台管理系统来说,通常左侧有一系列的操作tab。而每个tab对应一个组件。
当用户进入时,如果把所有的组件都进行加载,会导致用户体验下降,等待时间过长。所以就需要使用异步组件。
在用户进入页面时,只把它一开始用到的组件做成同步的。这样既不影响用户的操作体验,也优化了页面的加载速度。
Vue中,如果有多个vm实例都用到了异步组件,异步组件只会加载一次,并在内存中做缓存,不会重复加载。
异步组件如何使用?
注册异步组件,可注册普通组件差不太多。既可以全局注册,也可以局部注册。不过不同的是异步组件需要通过webpack的import函数来引入。如下:
1 | // 局部注册 |
其中,异步组件还有高级异步组件。所谓高级异步组件,就是提供了loading组件(用于组件异步加载时展示),error组件(用于组件加载出错时展示),delay(用于延迟加载异步组件),timeout(用于设置异步组件加载的最大时长,如果超过了规定的时间还没有加载完毕,则展示error组件)。
其中,error和loading组件都是同步的。因为当异步组件加载中或者加载出错时,需要立即展示对应的状态组件。
对于异步组件,通过使用webpack提供的import函数,可以让webpack在打包时将import中传入的组件文件单独打包。这样当使用到该组件时,通过发送请求异步加载组件的js,当资源加载完毕后交给vue来处理加载。
异步组件是怎样执行的?
首先,要先对webpack的import做一个说明:
根据webpack官网的解释,import是用于动态代码拆分。它传入一个模块路径,然后返回一个promise实例。在webpack打包时,
会将import引入的模块单独打包到一个代码包中。并且在代码执行到所在行时再去加载对应的资源。
对于webpack的import的探究,请参考模块方法。
下面来分析下异步组件究竟是怎么加载的。以上面用法中的全局异步组件为例:
Vue.component会在Vue.options.components上加入 {‘async-comp’: fn }。以便在组件渲染时执行vm._render方法时在createElement中找到对应的组件定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14const ASSET_TYPES = ['component', 'directive', 'filter'];
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
// ... 略去和流程无关部分
this.options[type + 's'][id] = definition
// 执行过后,Vue.options.components['async-comp'] = fn
return definition
}
}
})
}在createElement中调用createComponent函数,进入组件vnode的创建过程。在createComponent中,对于异步组件的处理,主要是resolveAsyncComponent函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46// 此处的调用关系:vm.$mount -> mountComponent -> vm._update(vm._render(), hydrating) ->
// -> vm._render -> vm._c -> createElement -> _createElement -> createComponent
// 具体的流程就不具体分析了,最终会进入到createComponent函数中。
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
// 此时 Ctor是之前定义的异步加载函数,并不是object
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// 有删减
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
// 最终会进入这里。
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
// 第一次执行渲染函数时,如果不是高级异步组件或者高级异步组件的delay不为0,则ctor默认为undefined。返回一个空的占位节点
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
)
}
}
// ... 省略其余部分
const vnode = new VNode(...opts);
return vnode;
}在resolveAsyncComponent函数中,做了下面几件事:
- 判断该异步组件的error及resolved,如果有error或isloading为true或者resolved不为undefined,则返回对应状态的组件。
- 收集了引用该异步组件的所有的vm实例,并存储在factory.owners中。
- 定义了forceRender函数(用于在加载成功或者失败后对owners遍历调用其渲染函数),resolve(异步组件加载成功后的处理函数),reject(异步组件加载失败后的处理函数)
- 调用异步组件对应的函数,并对高级异步组件做进一步的处理。
1 | export function resolveAsyncComponent ( |
所以异步组件的加载和执行实际上是通过执行两次渲染函数实现的。
第一次执行的时候,对异步组件对应的函数进行处理,定义处理loading, error状态组件,并给factory传入resolve, reject。当异步组件文件加载完成后通过resolve或者reject函数进行相应的处理,此时factory的error或者resolved状态会发生变化。设置完factory.error或者factory.resolved后,会调用forceRender,触发对应vm实例的渲染watcher的update,重新执行渲染,进而执行第二次渲染。
第二次渲染时,同样会进入resolveAsyncComponent方法中。此时异步组件的加载结果已经确定了,会返回对应的成功状态组件(即我们定义的异步组件)或者失败状态的组件。之后接着执行后续流程。
对异步组件的探究就先到这里了…