异步组件三种方式

  1. 普通函数异步组件
1
2
3
4
5
6
7
8
9
10
11
12
13
Vue.component('aa', function(resolve, reject) {
setTimeout(function() {
resolve({
template: '<div><p>{{aa}}<span>{{bb}}</span></p></div>',
data() {
return {
aa: '欢迎',
bb: 'Vue'
}
}
})
}, 1000)
})
  1. Promise 异步组件
1
Vue.component('aa', () => import('./aa.js') )
  1. 高级异步组件
1
2
3
4
5
6
7
8
9
10
11
12
13
const aa = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./aa.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('aa', aa)

从以上示例中可以看到,通过Vue.component注册的组件不再是一个对象,而是一个函数,这个函数也不是组件构造函数,是一个工厂函数。这个工厂函数有两个参数resolve函数和reject函数,其是 Vue 内部定义的,在这个工厂函数中有个异步函数,当异步函数执行成功后调用resolve函数,其参数就是异步组件的对象。

源码分析

组件的使用,要先在vm._render过程中执行vnode = createComponent(Ctor, data, context, children, tag)生成vnode,其中参数Ctor可以是函数或对象,从createComponent方法开始介绍异步组件是如何使用。

createComponent

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
function createComponent(Ctor, data, context, children, tag) {
if (isUndef(Ctor)) {
return
}
var baseCtor = context.$options._base
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
if (typeof Ctor !== 'function') {
return
}
// async component
var asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
}
data = data || {}
resolveConstructorOptions(Ctor)
installComponentHooks(data)
var name = Ctor.options.name || tag
var vnode = new VNode(
('vue-component-' + (Ctor.cid) + (name ? ('-' + name) : '')),
data, undefined, undefined, undefined, context, {
Ctor: Ctor,
tag: tag,
children: children
},
asyncFactory
)
return vnode
}

当参数Ctor值的类型是函数时,不会执行Ctor = baseCtor.extend(Ctor)

因为在 Vue 中是调用Vue.extend方法来创建继承 Vue 的组件构造函数。在Vue.extend中会执行Sub.cid = cid++给组件构造函数的cid属性赋值。

1
2
3
4
5
6
7
8
9
10
var cid = 1
Vue.extend = function(extendOptions) {
var Sub = function VueComponent(options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
return Sub
}

所以可以用isUndef(Ctor.cid)来判断Ctor是不是一个组件构造函数,若不是执行Ctor = resolveAsyncComponent(asyncFactory, baseCtor)进入异步组件使用的逻辑。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
function resolveAsyncComponent(factory, baseCtor) {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
var owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
if (owner && !isDef(factory.owners)) {
var owners = factory.owners = [owner]
var sync = true
var timerLoading = null
var timerTimeout = null
(owner).$on('hook:destroyed', function() {
return remove(owners, owner)
})
var forceRender = function(renderCompleted) {
for (var i = 0; l = owners.length; i < l i++) {
(owners[i]).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}
var resolve = once(function(res) {
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)
}
})
var res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else if (isPromise(res.component)) {
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 {
timerLoading = setTimeout(function() {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}
if (isDef(res.timeout)) {
timerTimeout = setTimeout(function() {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(
'timeout (' + (res.timeout) + 'ms)'
)
}
}, res.timeout)
}
}
}
sync = false
return factory.loading ? factory.loadingComp : factory.resolved
}
}

resolveAsyncComponent函数,是个高阶函数,主要对注册异步组件时,传入不同的工厂函数进行处理,内部定义了工厂函数的参数resolve函数和reject函数,并调用了工厂函数,成功执行resolve函数,失败执行reject函数,最后返回组件构造函数或 undefined。

异步组件和同步组件的注册原理是一样,只是异步组件的使用原理跟同步组件是不一样的。

异步组件的使用原理

1. 普通函数异步组件

1
2
3
4
5
6
7
8
9
10
11
12
13
Vue.component('aa', function(resolve, reject) {
setTimeout(function() {
resolve({
template: '<div><p>{{aa}}<span>{{bb}}</span></p></div>',
data() {
return {
aa: '欢迎',
bb: 'Vue'
}
}
})
}, 1000)
})

resolveAsyncComponent(factory, baseCtor),参数factory的值就是上面Vue.component的第二参数,参数baseCtor是 Vue 构造函数。

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
function resolveAsyncComponent(factory, baseCtor) {
if (isDef(factory.resolved)) {
return factory.resolved
}
var owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
if (owner && !isDef(factory.owners)) {
var owners = factory.owners = [owner]
var sync = true
(owner).$on('hook:destroyed', function() {
return remove(owners, owner)
})
var forceRender = function(renderCompleted) {
for (var i = 0; l = owners.length; i < l i++) {
(owners[i]).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
}
}
var resolve = once(function(res) {
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) : '')
)
})
var res = factory(resolve, reject)
sync = false
return factory.loading ? factory.loadingComp : factory.resolved
}
}

resolveAsyncComponent 函数中,内部定义了三个函数 forceRenderresolvereject。其中 resolvereject 函数是用 once 函数包装。

1
2
3
4
5
6
7
8
9
function once(fn) {
var called = false
return function() {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}

once 函数是个高阶函数,巧妙利用闭包和 called 变量,保证所包装的函数只执行一次。也就是确保 resolvereject 函数只执行一次。

因为在 resolveAsyncComponent 函数中最后执行 return factory.loading ? factory.loadingComp : factory.resolved,返回 factory.resolved

factory.resolved 定义在resolve函数中。

1
2
3
4
5
6
7
8
var resolve = once(function(res) {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})

执行 ensureCtor(res, baseCtor) 后赋值给 factory.resolved ,来看一下 ensureCtor 方法。

1
2
3
4
5
6
7
8
9
10
11
function ensureCtor(comp, base) {
if (
comp.__esModule ||
(hasSymbol && comp[Symbol.toStringTag] === 'Module')
) {
comp = comp.default
}
return isObject(comp) ?
base.extend(comp) :
comp
}

参数base为 Vue 构造函数,那么最后执行return isObject(comp) ? base.extend(comp) : comp,如果参数comp是个对象,执行base.extend(comp),也就是执行Vue.extend(comp)生成一个继承 Vue 的构造函数。

参数comp是通过resolve函数的参数res传参的。回到resolveAsyncComponent方法中,有执行var res = factory(resolve, reject)这段代码,factory是通过Vue.component的第二参数传参的,值如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
function(resolve, reject) {
setTimeout(function() {
resolve({
template: '<div><p>{{aa}}<span>{{bb}}</span></p></div>',
data() {
return {
aa: '欢迎',
bb: 'Vue'
}
}
})
}, 1000)
}

其中resolve就是resolveAsyncComponent函数内部定义的resolve函数。那么参数comp的值如下所示,是个组件的选项对象。

1
2
3
4
5
6
7
8
9
{
template: '<div><p>{{aa}}<span>{{bb}}</span></p></div>',
data() {
return {
aa: '欢迎',
bb: 'Vue'
}
}
}

这样执行ensureCtor(res, baseCtor)会得到一个组件构造函数,那么factory.resolved的值为一个组件构造函数。

再回到createComponent方法中,看这段代码

1
2
3
4
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}

执行resolveAsyncComponent方法后返回一个组件构造函数赋值给Ctor。就这么结束了吗。当然不是了,不知你有没有注意到在Vue.component定义的第二参数中,resolve(//...)外层还有一个setTimeout定时器,是个异步任务。JavaScript 是单线程的,异步任务要等所有同步任务都执行完才能执行。故此时resolveAsyncComponent方法中的resolve函数是不执行,factory.resolved应该为 undefined 。那么Ctor为 undefined ,要执行return createAsyncPlaceholder(asyncFactory, data, context, children, tag)createAsyncPlaceholder方法是用来创建一个注释节点vnode作为占位符。

1
2
3
4
5
6
7
8
9
10
11
function createAsyncPlaceholder(factory, data, context, children, tag) {
var node = createEmptyVNode()
node.asyncFactory = factory
node.asyncMeta = {
data: data,
context: context,
children: children,
tag: tag
}
return node
}

此时createComponent方法生成的一个注释节点vnode,而不是一个组件vnode,那组件要怎么渲染,不着急,再回到resolveAsyncComponent方法中,在 return 之前执行sync = false,在1000ms后resolve函数执行,会执行forceRender(true),来看一下forceRender函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var resolve = once(function(res) {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})
var owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
if (owner && !isDef(factory.owners)) {
var owners = factory.owners = [owner]
var forceRender = function(renderCompleted) {
for (var i = 0; l = owners.length; i < l i++) {
(owners[i]).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
}
}
}

currentRenderingInstance是使用异步组件的当前 Vue 实例,赋值给owner

如果同一个异步组件在很多个地方局部注册。这样要重复执行很多次相同的resolve函数。所以在这里做了个优化。

异步组件是以一个工厂函数factory来定义组件,在factory定义一个属性owners,来存储使用异步组件的当前 Vue 实例,也就是调用factory函数的上下文环境。

owner有值和factory.owners不存在,则说明factory函数是第一次执行。若owner有值和factory.owners有值,则说明factory函数已经执行过了。执行factory.owners.indexOf(owner) === -1判断factory.owners中有没有当前 Vue 实例,若没有,则把当前 Vue 实例添加到factory.owners中。

回到forceRender函数中,执行(owners[i]).$forceUpdate()相当执行vm.$forceUpdate()这个实例方法。这是因为异步组件加载过程中是没有数据发生变化的,所以要通过执行vm.$forceUpdate()迫使 Vue 实例重新渲染一次。

1
2
3
4
5
6
Vue.prototype.$forceUpdate = function() {
var vm = this
if (vm._watcher) {
vm._watcher.update()
}
}

执行vm._watcher.update()相当执行mountComponent方法中的vm._update(vm._render(), hydrating),在执行vm._render()过程中调用createComponent方法又执行到以下逻辑。

1
2
3
4
5
6
7
8
9
// async component
var asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
}

再次执行resolveAsyncComponent(asyncFactory, baseCtor)时,1000ms已过,故异步组件注册的工厂函数factory中的resolve函数已经执行完毕,故factory.resolved有值,直接返回factory.resolved

1
2
3
4
5
function resolveAsyncComponent(factory, baseCtor) {
if (isDef(factory.resolved)) {
return factory.resolved
}
}

2. Promise 异步组件

1
Vue.component('aa', () => import('./aa.js') )

resolveAsyncComponent(factory, baseCtor),参数baseCtor是 Vue 构造函数。参数factory的值就是上面Vue.component的第二参数,返回值是import('./aa.js'),它是一个 Promise 对象。整理一下代码,跟此场景无关的代码都去掉。

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
47
48
49
50
51
52
function resolveAsyncComponent(factory, baseCtor) {
if (isDef(factory.resolved)) {
return factory.resolved
}
var owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
if (owner && !isDef(factory.owners)) {
var owners = factory.owners = [owner]
var sync = true
(owner).$on('hook:destroyed', function() {
return remove(owners, owner)
})
var forceRender = function(renderCompleted) {
for (var i = 0; l = owners.length; i < l i++) {
(owners[i]).$forceUpdate()
}
if (renderCompleted) {
owners.length = 0
}
}
var resolve = once(function(res) {
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)
}
})
var res = factory(resolve, reject)
if (isObject(res)) {
if (isPromise(res)) {
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
}
}
sync = false
return factory.loading ? factory.loadingComp : factory.resolved
}
}

因为在此场景中工厂函数factory的返回值是一个 Promise 对象,所以满足isObject(res)isPromise(res)的条件,执行以下逻辑

1
2
3
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}

因为返回的是 Promise 对象,其实例方法then的参数是两个的函数resolvereject。当执行成功会执行resolve函数,当执行失败会调用reject函数。

所以这里巧妙地执行res.then(resolve, reject),当执行成功后会掉resolve函数,而这个resolve函数,是resolveAsyncComponent函数中自定义的。接下来的逻辑就和普通函数异步组件一模一样。

3. 高级异步组件

在高级异步组件中,可定义异步组件加载中展示的组件和加载失败展示的组件,对用户更友好。

1
2
3
4
5
6
7
8
9
10
11
12
13
const aa = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./aa.vue'),
// 加载中展示的组件
loading: LoadingComp,
// 加载失败展示的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('aa', aa)

resolveAsyncComponent(factory, baseCtor),参数baseCtor是 Vue 构造函数。参数factory的值就是上面Vue.component的第二参数,返回值是一个对象。整理一下代码,跟此场景无关的代码都去掉。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
function resolveAsyncComponent(factory, baseCtor) {
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isDef(factory.resolved)) {
return factory.resolved
}
var owner = currentRenderingInstance
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
factory.owners.push(owner)
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}
if (owner && !isDef(factory.owners)) {
var owners = factory.owners = [owner]
var sync = true
var timerLoading = null
var timerTimeout = null
(owner).$on('hook:destroyed', function() {
return remove(owners, owner)
})

var forceRender = function(renderCompleted) {
for (var i = 0; l = owners.length; i < l i++) {
(owners[i]).$forceUpdate()
}

if (renderCompleted) {
owners.length = 0
if (timerLoading !== null) {
clearTimeout(timerLoading)
timerLoading = null
}
if (timerTimeout !== null) {
clearTimeout(timerTimeout)
timerTimeout = null
}
}
}

var resolve = once(function(res) {
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)
}
})

var res = factory(resolve, reject)

if (isObject(res)) {
if (isPromise(res.component)) {
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 {
timerLoading = setTimeout(function() {
timerLoading = null
if (isUndef(factory.resolved) && isUndef(factory.error)) {
factory.loading = true
forceRender(false)
}
}, res.delay || 200)
}
}

if (isDef(res.timeout)) {
timerTimeout = setTimeout(function() {
timerTimeout = null
if (isUndef(factory.resolved)) {
reject(
"timeout (" + (res.timeout) + "ms)"
)
}
}, res.timeout)
}
}
}

sync = false
return factory.loading ? factory.loadingComp : factory.resolved
}
}

因为在此场景中工厂函数factory的返回值是一个对象res

若其中res.component属性是一个 Promise 对象,执行res.component.then(resolve, reject)

res.error有值,执行factory.errorComp = ensureCtor(res.error, baseCtor),把加载失败展示的组件转换成组件构造函数赋值给factory.errorComp

res.loading有值,执行factory.loadingComp = ensureCtor(res.loading, baseCtor),把加载中展示的组件转换成组件构造函数赋值给factory.loadingComp

res.delay的值为 0 ,则说明要直接展示加载中的组件,把factory.loading设置为 true 。

res.delay的值不为 0 ,则说明要经过一段delay时间的延迟才展示加载中的组件,用 setTimeout 定时器在经过一段delay时间的延迟,在异步组件没有加载成功或者失败的情况下把factory.loading设置为 true ,并执行forceRender(false),触发组件更新的 patch 过程渲染出加载中展示的组件。

res.timeout有值,用 setTimeout 定时器在在超出res.timeout后异步组件还未加载完成,报错一个超时的错误。

最后如果加载中组件构造函数factory.loading有值返回factory.loading,就不必调用createAsyncPlaceholder方法创建注释节点来作为占位节点,直接用加载中展示的组件生成的 DOM 节点来作为占位节点。

异步组件是在组件更新的 patch 过程才渲染的,会再调用resolveAsyncComponent方法。

1
2
3
4
5
6
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}

factory.error为true且加载失败组件构造函数factory.errorComp存在,返回factory.errorComp

factory.loading为true且加载中组件构造函数factory.loadingComp存在,返回factory.loadingComp

异步组件加载成功返回factory.resolved,接下来的逻辑就和普通函数异步组件一模一样。

最后在介绍一下异步组件加载失败时处理,其会调用自定义的reject函数,若factory.errorComp,把factory.error置为true。然后执行forceRender(true),此时其参数为true,在强制重新渲染中可以把加载中和加载超时中的定时器清空。

总结

Vue 的异步组件有 3 种实现方式,其中高级异步组件实现了loading、resolve、reject、timeout 4 种状态。异步组件实现的本质是 2 次渲染,除了delay为 0 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步加载组件成功后,执行resolve函数,在其中调用forceRender函数强制重新渲染,第二次调用resolveAsyncComponent函数,返回真正的组件构造函数factory.resolved,再通过组件更新的 patch 过程就能渲染出异步组件了。