使用场景:在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块,尤其是用在首屏优化。
异步组件注册传入的不是组件,而是一个工厂函数。
Vue 只有在这个组件 需要被渲染的时候才会触发该工厂函数 ,且会把 结果缓存起来供未来重渲染。
核心原理
调用栈:
- 页面渲染
- 调用render函数
- 调用createElement(参数是组件)
- 调用resolveAsyncComponent, 该函数内部定义了resolve/reject方法
- 执行组件的工厂函数,同时把 resolve 和 reject 函数作为参数传入,组件的工厂函数通常会先发送请求去加载我们的异步组件的 JS 文件,拿到组件定义的对象 res 后,执行 resolve(res) 逻辑
- 同步操作,第一次渲染会渲染出一个注释节点
- 执行异步操作, 执行resolve方法,缓存异步组件构造函数,resolve内调用forceUpdate
- 页面 第二次渲染
- 回到2-3-4 步骤,在resolveAsyncComponent拿到缓存的异步组件,返回,不执行后续操作
- 渲染出异步组件
原理图
核心代码
工具函数 once
通过闭包的形式,使得被once方法包装过的函数只能执行一次,在vue源码中对应的就是resolve方法和reject方法只会执行一次,这有利于前端节省性能开销。
1 | /** |
核心函数resolveAsyncComponent 核心代码
resolveAsyncComponentd的核心是resolve函数, 这个函数在拿到异步结果的时候被触发,主要逻辑:
- 传入的res是对象,是我们定义异步组件的export的内容,通过ensureCtor这个方法将res和baseCtor转化成一个VueComponent构造函数,实际上是调用Vue的全局静态方法extend(定义在global-api/中),VueComponent的原型会指向一个对象,这个对象的原型就是Vue的原型;这个VueComponent构造函数被缓存,作为factory.resolved的值。(子组件patch过程会执行实例化VueComponent构造函数,具体见函数createComponentInstanceForVnode,查看这里)
- 之前同步操作sync被至为false, 那么进入到resolve中,就会执行forceRender方法,即调用相关的vm实例的$forceUpdate方法再次渲染。
之后再次渲染又会进入resolveAsyncComponent的逻辑,发现isDef(factory.resolved)有定义,返回缓存的VueComponent构造函数,接下来便会被渲染
1 | export function resolveAsyncComponent ( |
异步组件加载的核心是resolve方法被调用,从vue官网我们可以看到有3种用工厂函数定义异步组件的方式:
- 工厂函数接受一个resolve函数作为参数,resovle函数作为回调函数会在从服务器得到组件定义的时候被调用调用。(一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用,见下面具体使用章节)
- 工厂函数返回一个promise
- 工厂函数返回一个对象,这个对象属性包括compnent/loading/error/timeout/delay, component的值是一个promise对象
区别:(详细见下方代码)
- 第一种方式在获取到 异步结果会直接执行resolve
- 第二种方式在 同步代码的时候res是个状态是pending的promise对象,resolve,reject在同步的时候被添加到这个promise的then中,在有异步结果的时候即pending状态改变的时候调用,promise状态是resolved调用resolve, reject状态调用reject。
- 第三种方式像是第二种方式的升级包装,同步代码直接拿到工厂函数的结果,是个对象
3.1 如果对象中有component属性而且component是个promise, 把resolve/reject函数添加到这个promise的then中,
3.2 如果定义了error属性和loading属性,则在这个工厂函数上定义error/loading的组件构造器;
3.3 如果定义了delay是0, 则第一次渲染会渲染出loading组件,否则设置定时器,判断在delay时间点后factory上如果没定义resolve和error, 渲染loading组件;
3.4 如果定义了timeOut, 则设置定时器,在timeout时间点如果没有factory上如果没定义resolve,执行reject, 渲染出error组件
3.5 异步加载组件成功,执行resovle, 在factory上定义resolve,缓存异步组件的构造函数,触发第二次渲染,渲染出异步组件
总结:
一般我们常用第二种,但是第三种功能配置更全面。
核心函数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
40if (isObject(res)) {
if (typeof res.then === 'function') {
// 第二种: 工厂函数返回promise () => 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)
}
}
}
具体使用
三种异步组件定义
工厂函数内执行 resolve 回调
1
2
3
4
5
6Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})工厂函数返回一个 Promise
1
2
3
4
5
6
7
8
9
10
11
12// 全局注册
Vue.component(
'async-webpack-example',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
// 局部注册
components: {
'my-component': () => import('./my-async-component')
}高级组件 工厂函数返回一个如下格式的对象
1
2
3
4
5
6
7
8
9
10
11
12
13const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})