vue异步组件原理

使用场景:在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块,尤其是用在首屏优化。
异步组件注册传入的不是组件,而是一个工厂函数。
Vue 只有在这个组件 需要被渲染的时候才会触发该工厂函数 ,且会把 结果缓存起来供未来重渲染。

核心原理

调用栈:

  1. 页面渲染
  2. 调用render函数
  3. 调用createElement(参数是组件)
  4. 调用resolveAsyncComponent, 该函数内部定义了resolve/reject方法
  5. 执行组件的工厂函数,同时把 resolve 和 reject 函数作为参数传入,组件的工厂函数通常会先发送请求去加载我们的异步组件的 JS 文件,拿到组件定义的对象 res 后,执行 resolve(res) 逻辑
  6. 同步操作,第一次渲染会渲染出一个注释节点
  7. 执行异步操作, 执行resolve方法,缓存异步组件构造函数,resolve内调用forceUpdate
  8. 页面 第二次渲染
  9. 回到2-3-4 步骤,在resolveAsyncComponent拿到缓存的异步组件,返回,不执行后续操作
  10. 渲染出异步组件

原理图


核心代码

工具函数 once
通过闭包的形式,使得被once方法包装过的函数只能执行一次,在vue源码中对应的就是resolve方法和reject方法只会执行一次,这有利于前端节省性能开销。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Ensure a function is called only once.
*/
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}

核心函数resolveAsyncComponent 核心代码

resolveAsyncComponentd的核心是resolve函数, 这个函数在拿到异步结果的时候被触发,主要逻辑:

  1. 传入的res是对象,是我们定义异步组件的export的内容,通过ensureCtor这个方法将res和baseCtor转化成一个VueComponent构造函数,实际上是调用Vue的全局静态方法extend(定义在global-api/中),VueComponent的原型会指向一个对象,这个对象的原型就是Vue的原型;这个VueComponent构造函数被缓存,作为factory.resolved的值。(子组件patch过程会执行实例化VueComponent构造函数,具体见函数createComponentInstanceForVnode,查看这里
  2. 之前同步操作sync被至为false, 那么进入到resolve中,就会执行forceRender方法,即调用相关的vm实例的$forceUpdate方法再次渲染。

之后再次渲染又会进入resolveAsyncComponent的逻辑,发现isDef(factory.resolved)有定义,返回缓存的VueComponent构造函数,接下来便会被渲染

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
export function resolveAsyncComponent (
factory: Function,
baseCtor: Class<Component>,
context: Component
): Class<Component> | void {

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

if (isDef(factory.contexts)) {
// already pending
factory.contexts.push(context)
} else {
const contexts = factory.contexts = [context]
let sync = true

const forceRender = () => {
for (let i = 0, l = contexts.length; i < l; i++) {
// 调用vue的强制渲染
contexts[i].$forceUpdate()
}
}

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()
}
})

// 执行我们组件的工厂函数,同时把 resolve 和 reject 函数作为参数传入,组件的工厂函数通常会先发送请求去加载我们的异步组件的 JS 文件,拿到组件定义的对象 res 后,执行 resolve(res) 逻辑
const res = factory(resolve, reject)

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

异步组件加载的核心是resolve方法被调用,从vue官网我们可以看到有3种用工厂函数定义异步组件的方式:

  1. 工厂函数接受一个resolve函数作为参数,resovle函数作为回调函数会在从服务器得到组件定义的时候被调用调用。(一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用,见下面具体使用章节)
  2. 工厂函数返回一个promise
  3. 工厂函数返回一个对象,这个对象属性包括compnent/loading/error/timeout/delay, component的值是一个promise对象

区别:(详细见下方代码)

  1. 第一种方式在获取到 异步结果会直接执行resolve
  2. 第二种方式在 同步代码的时候res是个状态是pending的promise对象,resolve,reject在同步的时候被添加到这个promise的then中,在有异步结果的时候即pending状态改变的时候调用,promise状态是resolved调用resolve, reject状态调用reject。
  3. 第三种方式像是第二种方式的升级包装,同步代码直接拿到工厂函数的结果,是个对象
    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
40
if (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)
}
}
}


具体使用

三种异步组件定义

  1. 工厂函数内执行 resolve 回调

    1
    2
    3
    4
    5
    6
    Vue.component('async-webpack-example', function (resolve) {
    // 这个特殊的 `require` 语法将会告诉 webpack
    // 自动将你的构建代码切割成多个包,这些包
    // 会通过 Ajax 请求加载
    require(['./my-async-component'], resolve)
    })
  2. 工厂函数返回一个 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')
    }
  3. 高级组件 工厂函数返回一个如下格式的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const AsyncComponent = () => ({
    // 需要加载的组件 (应该是一个 `Promise` 对象)
    component: import('./MyComponent.vue'),
    // 异步组件加载时使用的组件
    loading: LoadingComponent,
    // 加载失败时使用的组件
    error: ErrorComponent,
    // 展示加载时组件的延时时间。默认值是 200 (毫秒)
    delay: 200,
    // 如果提供了超时时间且组件加载也超时了,
    // 则使用加载失败时使用的组件。默认值是:`Infinity`
    timeout: 3000
    })