Vue 异步组件揭秘

1,363 阅读4分钟

异步组件

在学习异步组件之前,我想先和大家说以下webpack的动态加载,因为这个是实现异步组件的基础

webpack根据ES2015 loader 规范实现了用于动态加载的import()方法。

这个功能可以实现按需加载我们的代码,并且使用了promise式的回调,获取加载的包。

在代码中所有被import()的模块,都将打成一个单独的包,放在chunk存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载

首先我们看一下异步组件在Vue官网的教程用法

第一种用法:

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

第二种用法:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})

第三种用法:

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

我们首先看一下这个异步组件是在哪里调用的

src/core/vdom/create-component.js

  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

src/core/vdom/helpers/resolve-async-component.js

/* @flow */

import {
  warn,
  once,
  isDef,
  isUndef,
  isTrue,
  isObject,
  hasSymbol,
  isPromise,
  remove
} from 'core/util/index'

import { createEmptyVNode } from 'core/vdom/vnode'
import { currentRenderingInstance } from 'core/instance/render'

// 这个就是异步加载回来的组件,做了两个判断,如果是对象,就需要用extend去构建一个组件,否则直接用这个组件
function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}

export function createAsyncPlaceholder (
  factory: Function,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag: ?string
): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}

export function resolveAsyncComponent (
  factory: Function, // 就是我们传入的加载异步组件的函数,eg: () => import('A')
  baseCtor: Class<Component> // Vue
): Class<Component> | void {
  //下面的reject会把factory.error赋值为false, 如果加载失败了并且定义错误提示组件,那么返回错误提示组件
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }
  // 如果加载成功了,直接用返回的结果,这个resolved就是下面factory.resolved = ensureCtor(res, baseCtor)
  if (isDef(factory.resolved)) {
    return factory.resolved
  }

 // 这个就是拥有这个组件的组件
  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    // already pending
    factory.owners.push(owner)
  }
  // 如果是加载中,并且有定义了loading组件,那么直接返回loadingComp
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  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) => {
      for (let i = 0, l = owners.length; i < l; i++) {
        (owners[i]: any).$forceUpdate()
      }

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

    // 加载成功的操作
    const resolve = once((res: Object | Class<Component>) => {
      // cache resolved
      // 这个块的作用就是:如果我们写的组件就是一份opiton,那么要用Vue.extend去构建我们的组件Class,如果就是一个组件Class的话直接用就ok
      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
      }
    })

    // 失败的一个操作
    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)
      }
    })

    // 这个就调用了我们声明的异步组件,webpack遇到这里会直接去远程加载之前打包好的异步组件
    const res = factory(resolve, reject)

    if (isObject(res)) {
      // 这里就是上边说的异步组件的第二种方式的一个处理
      if (isPromise(res)) {
        // () => Promise
        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(() => {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)
          }
        }

        // 如果设置了超时时间的话,超了时间就会调用reject
        if (isDef(res.timeout)) {
          timerTimeout = setTimeout(() => {
            timerTimeout = null
            if (isUndef(factory.resolved)) {
              reject(
                process.env.NODE_ENV !== 'production'
                  ? `timeout (${res.timeout}ms)`
                  : null
              )
            }
          }, res.timeout)
        }
      }
    }

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