为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件来按需加载
Vue也原生支持了异步组件的能力

  1. Vue.component('async-example', function (resolve, reject) {
  2. // 这个特殊的 require 语法告诉 webpack
  3. // 自动将编译后的代码分割成不同的块,
  4. // 这些块将通过 Ajax 请求自动下载。
  5. require(['./my-async-component'], resolve)
  6. })

Vue注册的组件不再是一个对象而是一个工厂函数,函数参数resolve和reject,函数内部用setTimeout模拟了异步,实际使用可能是通过动态请求异步组件的js地址,最终通过执行resolve方法,它的参数就是异步组件对象

异步组件实现

createComponent函数,定义在src/core/vdom/create-component.js

  1. export function createComponent (
  2. Ctor: Class<Component> | Function | Object | void,
  3. data: ?VNodeData,
  4. context: Component,
  5. children: ?Array<VNode>,
  6. tag?: string
  7. ): VNode | Array<VNode> | void {
  8. if (isUndef(Ctor)) {
  9. return
  10. }
  11. const baseCtor = context.$options._base
  12. // plain options object: turn it into a constructor
  13. if (isObject(Ctor)) {
  14. Ctor = baseCtor.extend(Ctor)
  15. }
  16. // if at this stage it's not a constructor or an async component factory,
  17. // reject.
  18. if (typeof Ctor !== 'function') {
  19. if (process.env.NODE_ENV !== 'production') {
  20. warn(`Invalid Component definition: ${String(Ctor)}`, context)
  21. }
  22. return
  23. }
  24. // async component
  25. let asyncFactory
  26. // Ctor是一个函数且不会执行Vue.extend逻辑,所以Ctor.id是undefined
  27. if (isUndef(Ctor.cid)) {
  28. asyncFactory = Ctor
  29. Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  30. if (Ctor === undefined) {
  31. // return a placeholder node for async component, which is rendered
  32. // as a comment node but preserves all the raw information for the node.
  33. // the information will be used for async server-rendering and hydration.
  34. return createAsyncPlaceholder(
  35. asyncFactory,
  36. data,
  37. context,
  38. children,
  39. tag
  40. )
  41. }
  42. }
  43. // ...
  44. }

resolveAsyncComponent方法,定义在src/core/vdom/helpers/resolve-async-component.js中

  1. export function resolveAsyncComponent (
  2. factory: Function,
  3. baseCtor: Class<Component>
  4. ): Class<Component> | void {
  5. if (isTrue(factory.error) && isDef(factory.errorComp)) {
  6. return factory.errorComp
  7. }
  8. if (isDef(factory.resolved)) {
  9. return factory.resolved
  10. }
  11. const owner = currentRenderingInstance
  12. if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
  13. // already pending
  14. factory.owners.push(owner)
  15. }
  16. if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
  17. return factory.loadingComp
  18. }
  19. if (owner && !isDef(factory.owners)) {
  20. const owners = factory.owners = [owner]
  21. let sync = true
  22. let timerLoading = null
  23. let timerTimeout = null
  24. ;(owner: any).$on('hook:destroyed', () => remove(owners, owner))
  25. const forceRender = (renderCompleted: boolean) => {
  26. for (let i = 0, l = owners.length; i < l; i++) {
  27. (owners[i]: any).$forceUpdate()
  28. }
  29. if (renderCompleted) {
  30. owners.length = 0
  31. if (timerLoading !== null) {
  32. clearTimeout(timerLoading)
  33. timerLoading = null
  34. }
  35. if (timerTimeout !== null) {
  36. clearTimeout(timerTimeout)
  37. timerTimeout = null
  38. }
  39. }
  40. }
  41. const resolve = once((res: Object | Class<Component>) => {
  42. // cache resolved
  43. factory.resolved = ensureCtor(res, baseCtor)
  44. // invoke callbacks only if this is not a synchronous resolve
  45. // (async resolves are shimmed as synchronous during SSR)
  46. if (!sync) {
  47. forceRender(true)
  48. } else {
  49. owners.length = 0
  50. }
  51. })
  52. const reject = once(reason => {
  53. process.env.NODE_ENV !== 'production' && warn(
  54. `Failed to resolve async component: ${String(factory)}` +
  55. (reason ? `\nReason: ${reason}` : '')
  56. )
  57. if (isDef(factory.errorComp)) {
  58. factory.error = true
  59. forceRender(true)
  60. }
  61. })
  62. const res = factory(resolve, reject)
  63. if (isObject(res)) {
  64. if (isPromise(res)) {
  65. // () => Promise
  66. if (isUndef(factory.resolved)) {
  67. res.then(resolve, reject)
  68. }
  69. } else if (isPromise(res.component)) {
  70. res.component.then(resolve, reject)
  71. if (isDef(res.error)) {
  72. factory.errorComp = ensureCtor(res.error, baseCtor)
  73. }
  74. if (isDef(res.loading)) {
  75. factory.loadingComp = ensureCtor(res.loading, baseCtor)
  76. if (res.delay === 0) {
  77. factory.loading = true
  78. } else {
  79. timerLoading = setTimeout(() => {
  80. timerLoading = null
  81. if (isUndef(factory.resolved) && isUndef(factory.error)) {
  82. factory.loading = true
  83. forceRender(false)
  84. }
  85. }, res.delay || 200)
  86. }
  87. }
  88. if (isDef(res.timeout)) {
  89. timerTimeout = setTimeout(() => {
  90. timerTimeout = null
  91. if (isUndef(factory.resolved)) {
  92. reject(
  93. process.env.NODE_ENV !== 'production'
  94. ? `timeout (${res.timeout}ms)`
  95. : null
  96. )
  97. }
  98. }, res.timeout)
  99. }
  100. }
  101. }
  102. sync = false
  103. // return in case resolved synchronously
  104. return factory.loading
  105. ? factory.loadingComp
  106. : factory.resolved
  107. }
  108. }

处理了3种异步组件的创建方式

  1. 普通函数异步组件

    1. Vue.component('async-example', function (resolve, reject) {
    2. // 这个特殊的 require 语法告诉 webpack
    3. // 自动将编译后的代码分割成不同的块,
    4. // 这些块将通过 Ajax 请求自动下载。
    5. require(['./my-async-component'], resolve)
    6. })
  2. Promise异步组件

    1. Vue.component(
    2. 'async-webpack-example',
    3. // 该 `import` 函数返回一个 `Promise` 对象。
    4. () => import('./my-async-component')
    5. )
  3. 高级异步组件

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

    普通函数异步组件

    resolve和reject函数用once函数做了一层包装,定义在src/shared/util.js中

    1. /**
    2. * Ensure a function is called only once.
    3. * 利用闭包和一个标志位保证了它包装的函数只会执行一次
    4. */
    5. export function once (fn: Function): Function {
    6. let called = false
    7. return function () {
    8. if (!called) {
    9. called = true
    10. fn.apply(this, arguments)
    11. }
    12. }
    13. }

    确保resolve和reject函数只执行一次
    执行constres = factory(resolve, reject)就是执行组件的工厂函数,同时把resolve和reject函数作为参数传入,组件的工厂函数通常会发送请求去加载异步组件的js文件,拿到组件定义的对象res后执行resolve(res),会先执行factory.resolved = ensureCtor(res, baseCtor)

    1. // 为了保证能找到异步组件js定义的组件对象
    2. // 如果它是一个普通对象则调用Vue.extend把它转换成一个组件的构造函数
    3. function ensureCtor (comp: any, base) {
    4. if (
    5. comp.__esModule ||
    6. (hasSymbol && comp[Symbol.toStringTag] === 'Module')
    7. ) {
    8. comp = comp.default
    9. }
    10. return isObject(comp)
    11. ? base.extend(comp)
    12. : comp
    13. }

    之后resolve中判断了sync,该场景下sync为false,那么就会执行forceRender函数

    1. const forceRender = (renderCompleted: boolean) => {
    2. // 遍历factory.contexts,拿到每个调用异步组件的实例vm - owners[i]
    3. for (let i = 0, l = owners.length; i < l; i++) {
    4. (owners[i]: any).$forceUpdate()
    5. }
    6. if (renderCompleted) {
    7. owners.length = 0
    8. if (timerLoading !== null) {
    9. clearTimeout(timerLoading)
    10. timerLoading = null
    11. }
    12. if (timerTimeout !== null) {
    13. clearTimeout(timerTimeout)
    14. timerTimeout = null
    15. }
    16. }
    17. }

    $forceUpdate()定义在src/core/instance/lifecycle.js

    1. Vue.prototype.$forceUpdate = function () {
    2. const vm: Component = this
    3. // 调用渲染watcher的update方法,让渲染wacher对应的回调函数执行,也就是触发了组件的重新渲染
    4. if (vm._watcher) {
    5. vm._watcher.update()
    6. }
    7. }

    Vue通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行$forceUpdate可以强制组件重新渲染一次

    Promise异步组件

    1. Vue.component(
    2. 'async-webpack-example',
    3. // 该 `import` 函数返回一个 `Promise` 对象。
    4. () => import('./my-async-component')
    5. )

    webpack 2+ 支持了异步加载的语法糖:() => import(‘./my-async-component’)
    当执行完 res = factory(resolve, reject),返回的值就是 import(‘./my-async-component’) 的返回值,它是一个 Promise 对象

    1. if (isObject(res)) {
    2. if (isPromise(res)) {
    3. // () => Promise
    4. if (isUndef(factory.resolved)) {
    5. res.then(resolve, reject)
    6. }
    7. // ...
    8. }
    9. }

    当组件异步加载成功后执行resolve,加载失败则执行reject,巧妙实现了配合webpack2+ 的异步加载组件的方式(Promise)加载异步组件

    高级异步组件

    Vue.js 2.3+支持了一种高级异步组件的方式,通过一个简单的对象配置搞定loading组件和error组件的渲染时机

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

    高级异步组件的初始化逻辑和普通异步组件一样,也是执行 resolveAsyncComponent,当执行完 res = factory(resolve, reject),返回值就是定义的组件对象
    满足isPromise(res.component)为true

    1. if (isObject(res)) {
    2. if (isPromise(res)) {
    3. // ...
    4. } else if (isPromise(res.component)) {
    5. // 异步组件加载成功后执行resolve,失败执行reject
    6. res.component.then(resolve, reject)
    7. }
    8. }

    异步组件加载是一个异步的过程,接着又同步执行

    1. // 判断是否定义了error组件
    2. if (isDef(res.error)) {
    3. factory.errorComp = ensureCtor(res.error, baseCtor)
    4. }
    5. // 判断是否定义了loading组件
    6. if (isDef(res.loading)) {
    7. factory.loadingComp = ensureCtor(res.loading, baseCtor)
    8. // 如果delay配置为0,则这次直接渲染loading组件,否则延时delay执行forceRender,那么又会再一次执行到resolveAsyncComponent
    9. if (res.delay === 0) { // 是否设置看res.delay且为0
    10. factory.loading = true
    11. } else { // 否则延时delay的时间执行
    12. timerLoading = setTimeout(() => {
    13. timerLoading = null
    14. if (isUndef(factory.resolved) && isUndef(factory.error)) {
    15. factory.loading = true
    16. forceRender(false)
    17. }
    18. }, res.delay || 200)
    19. }
    20. }
    21. // 是否配置了timeout
    22. if (isDef(res.timeout)) {
    23. timerTimeout = setTimeout(() => {
    24. timerTimeout = null
    25. // 如果组件没有成功加载执行reject
    26. if (isUndef(factory.resolved)) {
    27. reject(
    28. process.env.NODE_ENV !== 'production'
    29. ? `timeout (${res.timeout}ms)`
    30. : null
    31. )
    32. }
    33. }, res.timeout)
    34. }

    最后一部分

    1. sync = false
    2. // return in case resolved synchronously
    3. return factory.loading
    4. ? factory.loadingComp
    5. : factory.resolved

    异步组件加载失败

    当异步组件加载失败会执行reject函数

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

    把factory.error设置为true,同时执行forceRender()再次执行到resolveAsyncComponent

    1. if (isTrue(factory.error) && isDef(factory.errorComp)) {
    2. return factory.errorComp
    3. }

    返回factory.errorComp,直接渲染error组件

    异步组件加载成功

    当异步组件加载成功会执行resolve函数

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

    把加载结果缓存到factory.resolved中,这时sync为false,则执行forceRender()再次执行到resolveAsyncComponent

    1. if (isDef(factory.resolved)) {
    2. return factory.resolved
    3. }

    直接返回factory.resolved,渲染成功加载的组件

    异步组件加载中

    如果异步组件加载中并未返回

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

    会返回factory.loadingComp,渲染loading组件

    异步组件加载超时

    如果超时则走到reject,和加载失败一样,渲染error组件

    异步组件patch

    如果第一次执行resolveAsyncComponent,除非使用高级异步组件 0 delay去创建了一个loading组件,否则返回的是undefined

    1. // async component
    2. let asyncFactory
    3. if (isUndef(Ctor.cid)) {
    4. asyncFactory = Ctor
    5. Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    6. if (Ctor === undefined) {
    7. // return a placeholder node for async component, which is rendered
    8. // as a comment node but preserves all the raw information for the node.
    9. // the information will be used for async server-rendering and hydration.
    10. // 通过createAsyncPlaceholder创建一个注释节点作为占位符
    11. return createAsyncPlaceholder(
    12. asyncFactory,
    13. data,
    14. context,
    15. children,
    16. tag
    17. )
    18. }
    19. }

    createAsyncPlaceholder方法定义在src/core/vdom/helpers/resolve-async-components.js中

    1. export function createAsyncPlaceholder (
    2. factory: Function,
    3. data: ?VNodeData,
    4. context: Component,
    5. children: ?Array<VNode>,
    6. tag: ?string
    7. ): VNode {
    8. const node = createEmptyVNode()
    9. node.asyncFactory = factory
    10. node.asyncMeta = { data, context, children, tag }
    11. return node
    12. }

    实际上就是创建一个占位的注释VNode,同时把asyncFactory和asyncMeta赋值给当前vnode
    当执行 forceRender 的时候,会触发组件的重新渲染,那么会再一次执行 resolveAsyncComponent,这时候就会根据不同的情况,可能返回 loading、error 或成功加载的异步组件,返回值不为 undefined,因此就走正常的组件 render、patch 过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode 的

    总结

    3 种异步组件的实现方式,并且看到高级异步组件的实现是非常巧妙的,它实现了 loading、resolve、reject、timeout 4 种状态
    异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了