菜鸟初探Vue源码(八)-- 异步组件

519 阅读3分钟

在开发过程中,随着工程越来越大,为了减少首屏js包的体积,可以使用异步组件的方式。Vue.js 实现了三种方式:工厂函数、Promise、高级异步组件。

工厂函数

Vue.component('HelloWorld', function (res, rej) {
  require(['./components/HelloWorld'], function (data) {
    res(data)
  })
})

以上为使用工厂函数的方式加载异步组件的示例。接下来我们探讨整个过程是如何实现的,在_createElement中通过resolveAsset()获取到 Ctor(上篇组件注册中有详解,即我们定义的工厂函数),因此在createComponent中,发现 Ctor 是一个函数,不会经过Vue.extend()处理,因此Ctor.cid为 undefined,因此会执行resolveAsyncComponent(asyncFactory, baseCtor),接下来看这个函数做了什么。

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // Vue
  const baseCtor = context.$options._base
  // Ctor is a function rather than an object
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  // async component
  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)
    }
  }
  // return a placeholder vnode
}

resolveAsyncComponent(asyncFactory, baseCtor)中,定义变量 owner,赋值为当前正在渲染的实例(vm)。factory 即为我们定义的工厂函数,最初factory.owners为 undefined,所以进入下面逻辑,定义forceRenderresolvereject函数,异步去执行factory(resolve, reject)resolveAsyncComponent函数执行完毕,返回undefined。回到createComponent,此时 Ctor 为 undefined,createAsyncPlaceholder执行,返回一个空节点(实际为注释节点,但保留了渲染真实 node 需要所有信息)。

factory(resolve, reject)中文件引入成功之后执行 resolve 函数,对组件的构造器做缓存之后执行forceRender,强制重新渲染。依照如下流程:$forceUpdate -> vm._watcher.update() -> vm._update(vm._render(), hydrating),最终会重新回到resolveAsyncComponent,此时判断factory.owners存在,直接返回,赋值给Ctor(此时产生了真实的组件构造器),获取到构造器之后就与同步加载的组件没有区别了。

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

  if (owner && !isDef(factory.owners)) {
    const owners = factory.owners = [owner]
    let sync = true
    const forceRender = (renderCompleted: boolean) => {
      for (let i = 0, l = owners.length; i < l; i++) {
        (owners[i]: any).$forceUpdate()
      }
    }
    const resolve = once((res: Object | Class<Component>) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor)
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })
    const reject = once(reason => {
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })
    const res = factory(resolve, reject)
    sync = false
    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}
Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
        vm._watcher.update()
    }
}

Promise

以下是一个使用Promise的示例。

Vue.component('HelloWorld', () => import('./components/HelloWorld.vue'))

由于工厂函数执行时import()会返回一个 Promise 对象,进入下面的 if 判断,调用 then 方法,其余流程与工厂函数的方式相同。

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
    // ...
    const res = factory(resolve, reject)
    if (isObject(res)) {
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      }
    }
}

高级异步组件

以下是一个高级异步组件的示例。

const LoadiComp = { template : '<div>Loading...</div>' }
const ErrorComp = { template : '<div>Error Occurred!</div>' }
const AsyncComp = () => ({
  component: import('./components/HelloWorld.vue'),
  loading: LoadiComp,
  error: ErrorComp,
  delay: 300,
  timeout: 1000
})
Vue.component('HelloWorld', AsyncComp)

在前面两种的基础上,高级异步组件又增加了loading组件、error组件、delay、timeout等。实现方式还是基于resolveAsyncComponent,工厂函数返回一个对象,此对象不是 Promise,但对象的 component 属性是一个 Promise,在此调用 then 方法。同时存储 error组件和 loading组件(如果配置中有的话),再通过 forceRender 重新渲染,等等。此处不做详细讲解了,如果还有疑问可以通过断点调试的方式,相信一定会恍然大悟。

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

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

  const owner = currentRenderingInstance
  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

    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
      factory.resolved = ensureCtor(res, baseCtor)
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })
    const reject = once(reason => {
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })
    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)
          }
        }

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

总的来说,异步组件的实现通常是2次渲染,先渲染成注释节点,组件加载成功后再通过forceRender重新渲染,这是异步组件的核心所在。