Vue.js源码学习 —— 异步组件的实现

如果您是刚开始准备阅读Vue.js的源码,建议先看一下本系列的Vue.js源码学习 —— 起步,相信会对您后面的阅读有很大帮助。

如果您才开始准备看Vue.js中虚拟DOM相关的内容,建议先看一下本系列的Vue.js源码学习 —— VNode/createElement,再来看今天这篇会轻松很多。

今天这篇文章是接着上一篇Vue.js源码学习 —— createComponent的,代码主要位于文件core/vdom/resolve-async-component.js中。

我没有在文章中大量的贴源码,一定要将源码clone到本地哦。

关于异步组件的介绍可以翻阅官方文档#异步组件一节。

resolveAsyncComponent

resolveAsyncComponent函数的实现位于文件core/vdom/resolve-async-component.js中。可以看到里面的实现相对复杂一些,这是因为Vue允许我们使用3种方式来创建异步组件,所以处理的情况较多。这3种方式包括:直接使用异步函数、使用Promise对象、使用包装了各种加载状态的对象(也就是高阶组件)。

咱们先从最简单的使用异步函数开始看。

传异步函数

先看个使用异步函数创建异步组件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const child = {
name: 'child',
props: ['msg'],
render (h) {
return h('p', `${this.msg}`)
}
}

// 异步函数
function async (resolve, reject) {
setTimeout(() => {
resolve(child)
}, 10)
}

const vm = new Vue({
el: '#app',
data: { message: 'hello world' },
render (h) {
return h(async, { props: { msg: this.message } }) // data
}
})

可以将其放到浏览器中调试看一下执行过程,比纯看代码容易理解一些。

此时会直接走到下面这个if中,上面的if都不满足条件:

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
if (owner && !isDef(factory.owners)) {
const owners = factory.owners = [owner]
let sync = true
let timerLoading = null
let timerTimeout = null

;(owner: any).$on('hook:destroyed', () => remove(owners, owner))

const forceRender = (renderCompleted: boolean) => {
// ...
}

const resolve = once((res: Object | Class<Component>) => {
// ...
})

const reject = once(reason => {
// ...
})

const res = factory(resolve, reject)

if (isObject(res)) {
// ...
}

sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved
}

owner是当前正在渲染的实例对象,在等异步的内容执行结束后,该owner会作为强制刷新的上下文对象。

先来看这一句:

1
const res = factory(resolve, reject)

在我们的例子中,resfactory函数(也就是例子中的async函数)执行完得到的值是undefined,所以isObject(res)false,走到了return语句。现在factoryloadingloadingCompresolved属性都没有值,所以resolveAsyncComponent函数最终返回了undefined

再来看resolve函数:

1
2
3
4
5
6
7
8
9
10
11
const resolve = once((res: Object | Class<Component>) => {
// cache resolved
factory.resolved = ensureCtor(res, baseCtor)
// invoke callbacks only if this is not a synchronous resolve
// (async resolves are shimmed as synchronous during SSR)
if (!sync) {
forceRender(true)
} else {
owners.length = 0
}
})

resolve函数做的事:

  • factory增加一个resolved的属性,来缓存这个异步组件的构造函数,等到这个组件到了真正要创建的时候会用到。

  • 如果是开发者传入的是异步的resolve,就调用forceRender重新走遍创建组件的流程。如果传入的是同步的resolve,那第一次执行resolveAsyncComponent函数时,就会将创建组件了,所以将owners.length设置为0。

    为什么同步的resolve,在执行到resolve时,synctrue呢?

    因为在执行factory(resolve, reject)这句话时,如果resolve是同步的,就会在factory内部执行resolve函数了,而此时sync还没有被设置成false

  • ensureCtor函数的作用就是确保能得到正确的构造器函数,像我们的例子中给resolve函数传入的是选项对象,ensureCtor函数内部就会调用extend方法用传入的选项对象生成一个构造器函数。

  • once函数的作用就是确保resolve只执行一次,这是有必要的,因为resolve的执行是开发者控制的。

再来看一下reject函数:

1
2
3
4
5
6
7
8
9
10
const reject = once(reason => {
process.env.NODE_ENV !== 'production' && warn(
`Failed to resolve async component: ${String(factory)}` +
(reason ? `\nReason: ${reason}` : '')
)
if (isDef(factory.errorComp)) {
factory.error = true
forceRender(true)
}
})

reject函数做的事:

  • 首先在开发环境下给出告警信息,组件创建失败啦。

  • 如果开发者设置error项,就会调用forceRender函数,这会导致创建开发设置的异常情况的组件。errorComp在下面的代码中会看到是何时放到factory上的。

forceRender函数中主要就干了两件事,调用owner$forceUpdate()方法重新触发创建组件。$forceUpdate()的实现等看到文件core/instance/render.js中的代码时再讲。但是通过调试看函数调用栈可以发现,这个方法会导致重新走进resolveAsyncComponent函数。

传Promise对象

再来看使用Promise对象创建异步组件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function async (resolve, reject) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// child 还是上面例子中的
resolve(child)
}, 10)
})
}

const vm = new Vue({
el: '#app',
data: { message: 'hello world' },
render (h) {
return h(async, { props: { msg: this.message } }) // data
}
})

这时就会走到下面这段代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (isObject(res)) {
if (isPromise(res)) {
// () => Promise
if (isUndef(factory.resolved)) {
res.then(resolve, reject)
}
} else {
// ...
}
}

sync = false
// return in case resolved synchronously
return factory.loading
? factory.loadingComp
: factory.resolved

首先判断factory是否已经有resolved属性了,如果已经有了,说resolve函数已经执行过了,也就是开发者是同步调用的resolve。如果是异步调用的resolve,就给该Promise对象添加了一个then方法,等待Promise内的异步事件执行结束后resolvereject就会被触发了。

所以可以看到,在resolve是异步调用的时候,此时resolveAsyncComponent函数还是返回undefined

传包装了加载状态的对象

还是先举个例子:

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
const LoadingComponent = {
name: 'LoadingComponent',
render (h) {
return h('p', 'loading component...')
}
}

const ErrorComponent = {
name: 'ErrorComponent',
render (h) {
return h('p', 'solve component error!')
}
}

const AsyncComponent = (resolve, reject) => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
// async函数还是上面例子中的
component: async(resolve, reject),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})

const vm = new Vue({
el: '#app',
data: { message: 'hello world' },
render (h) {
return h(AsyncComponent, { props: { msg: this.message } }) // data
}
})

这时候就会执行到下面这段代码中了:

1
2
3
4
5
6
7
8
if (isObject(res)) {
if (isPromise(res)) {
// ...
} else if (isPromise(res.component)) {
res.component.then(resolve, reject)
// ...
}
}

还是给Promise对象加上了then方法,以便在异步内容执行完后,调用resolvereject

接下来就是对传入了几个状态项进行处理:

  • 如果传了error,会获取对应组件的构造器放入factory.errorComp中。在reject被调用时,factory.error会被置为true

  • 如果传了delay,还是先获取对应组件的构造器放入factory.loadingComp中。然后判断delay为0时,会立即展示这个 loading组件,这时resolveAsyncComponent函数会返回该 loading组件 构造器函数。如果delay大于0,会开启定时器延迟展示 loading组件,这时resolveAsyncComponent函数会返回undefined

  • 如果传了timeout,会马上开启定时器计时,一定时间到了resolve还没有执行,就会调用reject,并在开发环境下给出告警信息。

现在再回头看下面几个if判断就明了了吧:

1
2
3
4
5
6
7
8
9
10
11
if (isTrue(factory.error) && isDef(factory.errorComp)) {
return factory.errorComp
}

if (isDef(factory.resolved)) {
return factory.resolved
}

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
return factory.loadingComp
}

createAsyncPlaceholder

从上面的分析可以看出resolveAsyncComponent函数只有在传了loading并且delay为0时,会返回 loading组件 的构造器函数,其他情况都是返回undefined

返回undefined时,在createComponent函数中,会调用createAsyncPlaceholder返回一个空节点,这样就不会渲染内容到页面上。

返回 loading组件 的构造器函数时,就会接着执行createComponent函数后面的代码了,最终返回这个组件的节点。

异步组件的实现过程到此就了解的差不多了,相信我们以后的开发中使用异步组件会更加灵活。

本文作者:意林
本文链接:http://shinancao.cn/2020/01/17/Vue-Source-Code-05/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!