在我们平时的开发工作中,为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件,按需加载。Vue 也原生支持了异步组件的能力。
Vue 支持三种异步组件方式:
工厂函数中使用
resolve
:1
2
3
4
5
6Vue.component('async-example', function (resolve, reject) {
// 这个特殊的 require 语法告诉 webpack
// 自动将编译后的代码分割成不同的块,
// 这些块将通过 Ajax 请求自动下载。
require(['./my-async-component'], resolve)
})工厂函数中使用
Promise
:1
2
3
4
5Vue.component(
'async-webpack-example',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)工厂函数使用
高级组件对象
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
Vue.component('async-webpack-example', AsyncComponent)
示例中可以看到,Vue 注册的组件不再是一个对象,而是一个工厂函数,函数有两个参数 resolve
和 reject
,函数内部用 setTimout
模拟了异步,实际使用可能是通过动态请求异步组件的 JS 地址,最终通过执行 resolve
方法,它的参数
就是我们的异步组件对象
。
在了解了异步组件如何注册后,我们从源码的角度来分析一下它的实现。
异步组件分析
上一节我们分析了组件的注册逻辑
1 | Vue.component("my-component", { |
由于组件的定义并不是一个普通对象, 所以不会执行 Vue.extend
的逻辑把它变成一个组件的构造函数,但是它仍然可以执行到 createComponent
函数,我们再来对这个函数做回顾,它的定义在 src/core/vdom/create-component/js
中:
1 | export function createComponent ( |
由于我们这个时候传入的 Ctor
是一个函数,那么它也并不会执行 Vue.extend
逻辑,因此它的 cid
是 undefiend
,进入了异步组件创建的逻辑。这里首先执行了 Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
方法,它的定义在 src/core/vdom/helpers/resolve-async-component.js
中:
1 | function resolveAsyncComponent( |
resolveAsyncComponent
函数的逻辑略复杂,因为它实际上处理了 3 种异步组件的创建方式,下面我们分别来看看这三种方式的源码流程
1. 工厂函数使用 resolve(普通函数异步组件)
还是这个例子:
1 | Vue.component('async-example', function (resolve, reject) { |
异步组件加载前:
1 | export function resolveAsyncComponent ( |
这里有个函数叫once
,对resolve
和 reject
函数做了一层包装:
1 | export function once (fn: Function): Function { |
once
逻辑非常简单,传入一个函数,并返回一个新函数,它非常巧妙地利用闭包
和一个标志位
保证了它包装的函数只会执行一次,也就是确保 resolve
和 reject
函数只执行一次。
- 如果当前异步组件已经初始化完毕,那么只需要往工厂函数的
owners
中push
当前渲染的实例 - 如果是第一次初始化异步组件时:
sync
表示当前是否同步执行- 定义了
forceRender
、resolve
和reject
函数 - 执行工厂函数,把
resolve
和reject
函数作为参数传入,并取得返回值res
isObject(res)
返回false
,因为在当前例子中工厂函数并没有返回值- 将
syn
c 置为false
- 返回
factory.resolved
,但此时还是undefined
再回到createComponent
1 | Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) |
因为此时 resolveAsyncComponent
函数返回了 undefined
,所以通过 createAsyncPlaceholder
创建一个注释节点
作为占位符(实际上就是就是创建了一个占位的注释VNode
,同时把 asyncFactory
和 asyncMeta
赋值给当前vnode
。)
当执行 forceRender
的时候,会触发组件的重新渲染,那么会再一次执行 resolveAsyncComponent
,这时候就会根据不同的情况,可能返回 loading
、error
或成功加载的异步组件,返回值不为 undefined,因此就走正常的组件 render
、patch
过程,与组件第一次渲染流程不一样,这个时候是存在新旧vnode
的。在组件更新patch过程中会讲到。
异步组件加载后:
当执行完同步逻辑后,我们再来看看异步加载后的流程:
在工厂函数中通常会先发送请求去加载我们的异步组件的 JS 文件,拿到组件定义的对象 res 后,执行 resolve(res) 逻辑。
resolve 函数源码如下:
1 | const resolve = once((res: Object | Class<Component>) => { |
1 | function ensureCtor (comp: any, base) { |
这个函数目的是为了保证能找到异步组件 JS 定义的组件对象,并且如果它是一个普通对象,则调用 Vue.extend
把它转换成一个组件的构造函数。
回到 resolve函数
,将返回的组件构造函数挂载到 factory.resolved
resolve 逻辑最后判断了 sync
,显然我们这个场景下 sync
为 false
,那么就会执行 forceRender
函数,它会遍历 factory.contexts
,拿到每一个调用异步组件的实例 vm, 执行 vm.$forceUpdate()
方法,它的定义在 src/core/instance/lifecycle.js
中:
1 | Vue.prototype.$forceUpdate = function () { |
$forceUpdate
的逻辑非常简单,就是调用渲染watcher
的 update
方法,让渲染watcher
对应的回调函数执行,也就是触发了组件的重新渲染。之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate
可以强制组件重新渲染一次。
2. 工厂函数中使用 Promise
1 | Vue.component( |
其实这种情况和第一个例子基本上相差无几,看下 resolveAsyncComponent
函数:
1 | // src/core/vdom/helpers/resolve-async-component.js |
当执行完 res = factory(resolve, reject)
,返回的值就是 import('./my-async-component')
的返回值,它是一个 Promise对象
。接着进入 if 条件:
- isObject(res) 为 true
- isPromise(res) 为 true
- isUndef(factory.resolved) 为 true
然后给 Promise 对象添加 then 方法: res.then(resolve, reject)
当组件异步加载成功后,执行 resolve,加载失败则执行 reject,这样就非常巧妙地实现了配合 webpack 2+ 的异步加载组件的方式(Promise)加载异步组件。
3. 工厂函数使用高级组件对象
1 | const AsyncComponent = () => ({ |
高级异步组件的初始化逻辑和普通异步组件一样,也是执行 resolveAsyncComponent
,当执行完 res = factory(resolve, reject)
,返回值就是定义的组件对象。接着进入 if 条件:
- isObject(res) 为 true
- isPromise(res) 为 false
显然满足 else 的逻辑,接着执行 res.component.then(resolve, reject)
,当异步组件加载成功后,执行 resolve,失败执行 reject。
因为异步组件加载是一个异步过程,它接着又同步执行了如下逻辑:
1 | if (isDef(res.error)) { |
- 先判断
res.error
是否定义了error
组件,如果有的话则赋值给factory.errorComp
。 - 接着判断
res.loading
是否定义了loading组件
,如果有的话则赋值给factory.loadingComp
如果设置了
res.delay
且为0
,则设置factory.loading = true
,否则延时delay
的时间执行:1
2
3
4if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}最后判断
res.timeout
,如果配置了该项,则在res.timout
时间后,如果组件没有成功加载,执行 reject。
在 resolveAsyncComponent
的最后有一段逻辑:
1 | sync = false |
如果 delay
配置为 0,则这次直接渲染 loading组件
,否则则延时 delay 执行 forceRender
,那么又会再一次执行到 resolveAsyncComponent
。
那么这时候我们有几种情况,按逻辑的执行顺序,对不同的情况做判断。
当异步组件加载失败,会执行 reject 函数:
1 | const reject = once(reason => { |
这个时候会把 factory.error
设置为 true,同时执行 forceRender()
再次执行到 resolveAsyncComponent
:
1 | if (isTrue(factory.error) && isDef(factory.errorComp)) { |
那么这个时候就返回 factory.errorComp,直接渲染 error 组件。
异步组件加载成功
1 | const resolve = once((res: Object | Class<Component>) => { |
首先把加载结果缓存到 factory.resolved
中,这个时候因为 sync
已经为 false
,则执行 forceRender()
再次执行到 resolveAsyncComponent
:
1 | if (isDef(factory.resolved)) { |
那么这个时候直接返回 factory.resolved,渲染成功加载的组件。
异步组件加载中
如果异步组件加载中并未返回,这时候会走到这个逻辑:
1 | if (isTrue(factory.loading) && isDef(factory.loadingComp)) { |
那么则会返回 factory.loadingComp
,渲染 loading组件
。
异步组件加载超时
如果超时,则走到了 reject 逻辑,之后逻辑和加载失败一样,渲染 error 组件。
总结:
通过以上代码分析,我们对 Vue 的异步组件的实现有了深入的了解,知道了 3 种异步组件的实现方式,并且看到高级异步组件的实现是非常巧妙的,它实现了 loading
、resolve
、reject
、timeout
4 种状态。异步组件实现的本质是 2 次渲染,除了 0 delay
的高级异步组件第一次直接渲染成 loading组件
外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender
强制重新渲染,这样就能正确渲染出我们异步加载的组件了。