Vue源码(八)异步组件原理

Vue中总共有3种异步组件,分别是普通函数异步组件、Promise异步组件和高级异步组件,接下来分别介绍一下它们的原理

普通函数异步组件

Vue.component('hello-world', function (resolve, reject) {
  require(['../components/HelloWorld'], resolve)
})
复制代码

这个require语法的执行逻辑是,先请求这个组件,请求成功后调用resolve函数。

先看下Vue.component的实现,在创建Vue函数时,会执行initGlobalAPI,里面会执行initAssetRegisters方法

// 注册 Vue.component,Vue.directive, Vue.filter 函数
initAssetRegisters(Vue)
复制代码

initAssetRegisters定义在src/core/global-api/assets.js

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]  
export function initAssetRegisters (Vue: GlobalAPI) {
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (id: string, definition: Function | Object) {}
  })
}
复制代码

initAssetRegisters方法会分别注册Vue.componentVue.directiveVue.filter

当通过Vue.component注册组件时,执行这个方法

function (id: string, definition: Function | Object) {
  if (!definition) {
    return this.options[type + 's'][id]
  } else {
    if (type === 'component' && isPlainObject(definition)) {
      // 全局注册的组件,如果是一个对象,则通过 Vue.extend 返回一个子组件的构造函数
      definition.name = definition.name || id
      definition = this.options._base.extend(definition)
    }
    if (type === 'directive' && typeof definition === 'function') {
      definition = { bind: definition, update: definition }
    }
    this.options[type + 's'][id] = definition
    return definition
  }
}
复制代码

首先如果没有传入definition则去this.options.components中查找这个组件并返回;反之如果definition是一个普通对象,则通过Vue.extend返回一个子组件的构造函数;并将这个子组件构造函数添加到this.options.components

当注册普通函数异步组件时,由于传入的是一个函数,所以不会创建组件构造函数,而是直接将传入的函数挂载到Vue.options.components中。

当调用createComponent创建组件VNode时,有如下逻辑

const baseCtor = context.$options._base
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}

let asyncFactory
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  if (Ctor === undefined) {
    return createAsyncPlaceholder(
      asyncFactory,
      data,
      context,
      children,
      tag
    )
  }
}
复制代码

如果传入的Ctor是对象,创建组件构造函数;如果Ctor.cid没有值说明Ctor是一个自定义函数(大概率是异步组件),因为如果传入的是一个对象会通过Vue.extend创建组件构造函数,并有一个cid属性。

接下来调用resolveAsyncComponent方法

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
  // ...
  
  // currentRenderingInstance 在 _render 函数中被赋值,指向当前的 Vue 实例
  const owner = currentRenderingInstance
  // 当多个组件都使用当前异步组件时,将使用该组件的Vue实例收集到 factory.owners 中,最后统一执行
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    factory.owners.push(owner)
  }

	// ...

  if (owner && !isDef(factory.owners)) {
    // 将当前 Vue 实例放到 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 factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}
复制代码

对于普通函数的异步组件,获取当前正在渲染的Vue实例并赋值给owner。如果factory.owners为空,定义一个局部变量sync,执行factory并传入resolvereject,也就是说执行传入的组件函数

Vue.component('hello-world', function (resolve, reject) {
  require(['../components/HelloWorld'], resolve)
})
复制代码

当异步组件加载完成之后会调用resolve函数,这个之后再说。在加载异步组件期间会继续往下执行,首先将sync设置为false,由于普通函数异步组件返回值为undefined,所以if (isObject(res))内的逻辑不会执行,直接返回一个undefined;回到createComponent方法

const baseCtor = context.$options._base
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}

let asyncFactory
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
  // 这里开始
  if (Ctor === undefined) {
    return createAsyncPlaceholder(
      asyncFactory,
      data,
      context,
      children,
      tag
    )
  }
}
复制代码

此时Ctorundefined,通过createAsyncPlaceholder方法创建一个注释VNode,表示这里的位置是异步组件的位置

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
}
复制代码

createAsyncPlaceholder方法就是创建一个注释VNode,并给这个注释VNode添加asyncFactory属性、asyncMeta属性

在普通异步组件加载过程中,如果其他组件也使用了这个异步组件也会调用resolveAsyncComponent方法,并进入下面逻辑

  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    factory.owners.push(owner)
  }
	// ...

  if (owner && !isDef(factory.owners)) {
    // ...
  }
复制代码

将组件的Vue实例添加到factory.owners中,由于此时factory.owners已经有值了,所以返回undefined,并在createComponent方法中创建一个注释VNode。

当普通函数异步组件加载完成之后,会调用前面说的resolve函数

const resolve = once((res: Object | Class<Component>) => {
  // cache resolved
  factory.resolved = ensureCtor(res, baseCtor)
  if (!sync) {
    forceRender(true)
  } else {
    owners.length = 0
  }
})
复制代码

resolve函数是once函数的返回值,once函数的作用就是保证传入的函数只调用一次,也就是说resolvereject只执行一次

function once (fn) {
  var called = false;
  return function () {
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  }
}
复制代码

resolve函数内调用ensureCtor函数获取异步组件的构造函数,并将获取的构造函数挂载到factory.resolved

function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}
复制代码

普通函数异步组件是通过require方式加载的,而webpack在编译过程中会实现require方法,给导出的内容添加一个__esModule属性,并修改导出内容的Symbol.toStringTag属性的值。

ensureCtor函数就是获取导出内容,如果导出内容是一个函数则直接返回,如果是一个对象则调用Vue.extend方法创建组件的构造函数

回到resolve函数中,此时变量syncfalse,会执行forceRender(true)方法

const forceRender = (renderCompleted: boolean) => {
  for (let i = 0, l = owners.length; i < l; i++) {
    (owners[i]: any).$forceUpdate()
  }

  // 如果组件加载成功或加载失败,则传入 true。清空 loading 和 timeout 的定时器
  // 如果是 loading状态 则 传入 false
  if (renderCompleted) {
    owners.length = 0
    if (timerLoading !== null) {}
    if (timerTimeout !== null) {}
  }
}
复制代码

forceRender函数内遍历所有使用到该异步组件的组件实例,并调用它们的$forceUpdate触发这些组件更新。在重新创建VNode过程中,再次调用resolveAsyncComponent方法,里面有这样一段逻辑

if (isDef(factory.resolved)) {
  return factory.resolved
}
复制代码

此时factory.resolved有值了,值为异步组件的构造函数,所以这里就和创建正常组件VNode流程一样了。当所有VNode都创建完成之后,进入patch过程,通过sameVnode比对异步组件VNode和注释VNode时,返回false,重新创建异步组件的DOM树,并删除注释VNode

Promise 异步组件

Vue.component('hello-world', () => import('./components/HelloWorld'))
复制代码

Promise 异步组件的函数中,会返回一个Promise,所以在resolveAsyncComponent方法中的处理方式和普通函数异步组件不同

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 (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    factory.owners.push(owner)
  }
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }
  if (owner && !isDef(factory.owners)) {
    // 将当前 Vue 实例放到 factory.owners 中
    const owners = factory.owners = [owner]
    let sync = true
    let timerLoading = null
    let timerTimeout = null
    const forceRender = (renderCompleted: boolean) => {}
    const resolve = once((res: Object | Class<Component>) => {})
    const reject = once(reason => {})
    // 这里返回的是 Promise
    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)) {
        // ...
      }
    }
    sync = false
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}
复制代码

当调用factory时,会返回一个Promise对象,所以if (isObject(res))、if (isPromise(res))都为true,并且if (isUndef(factory.resolved))也为true,所以会给这个res添加then方法,并将resolve, reject传入。最后resolveAsyncComponent也返回一个undefinedcreateComponent方法会创建一个注释节点。

当组件加载成功之后调用resolve函数,函数内遍历组件实例并调用$forceUpdate方法触发更新。

高级异步组件

const LoadingComp = {
  template: '<div>loading...</div>'
}
const ErrorComp = {
  template: '<div>error...</div>'
}
const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('../components/HelloWorld'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})
Vue.component('hello-world', AsyncComp)
复制代码

高级异步组件可以设置中间状态,比如加载状态、出错状态,这些状态的加载也是在resolveAsyncComponent

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
  }
  // currentRenderingInstance 在 _render 函数中被赋值,指向当前的 Vue 实例
  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    factory.owners.push(owner)
  }
  // 加载中
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (owner && !isDef(factory.owners)) {
    // 将当前 Vue 实例放到 factory.owners 中
    const owners = factory.owners = [owner]
    let sync = true
    let timerLoading = null
    let timerTimeout = null

    const forceRender = (renderCompleted: boolean) => {}
    const resolve = once((res: Object | Class<Component>) => {})
    const reject = once(reason => {})

    const res = factory(resolve, reject)

    if (isObject(res)) {
      if (isPromise(res)) {
        // ...
        
      } else if (isPromise(res.component)) {
        // ...
        
      }
    }

    sync = false
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}
复制代码

对于高级异步组件也是先执行 const res = factory(resolve, reject)加载组件并且返回值是一个对象,所以else if (isPromise(res.component))true进入下面逻辑

else if (isPromise(res.component)) {
  res.component.then(resolve, reject)
  if (isDef(res.error)) {
    factory.errorComp = ensureCtor(res.error, baseCtor)
  }
  
  // ...
复制代码

res.component添加then方法,当组件加载完成后执行resolve函数。如果设置了error属性,获取error对应的组件内容,并挂载到factory.errorComp上。继续执行

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)
  }
}
// ...
复制代码

如果有loading属性,获取loading对应的组件内容,并挂载到factory.loadingComp上。如果有delay属性并且值为0,将factory.loading设置为true,说明要立马渲染loading组件。否则添加一个延时时间为delay的定时器,表示delay毫秒后才会渲染loading组件;默认是200ms。继续执行

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)
}
// ...
复制代码

如果设置了timeout属性,设置一个延时时间为timeout的定时器,表示超时时间。继续执行

sync = false
return factory.loading
  ? factory.loadingComp
: factory.resolved
复制代码

上面说过如果设置的delay0factory.loadingtrue,所以会直接返回loading组件,而不是创建一个注释VNode

接下来的几种情况

加载超时

加载超时会执行延时时间为timeout的定时器,清空定时器,并执行reject函数

reject函数如下,如果配置了error组件则将factory.error = true,并调用forceRender函数

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)
  }
})
复制代码

forceRender函数就是让组件更新,更新过程重新进入resolveAsyncComponent方法,返回error组件

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

回到forceRender,因为传入的参数为true,所以会执行剩余逻辑

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
    }
  }
}
复制代码

if里面的逻辑是清空owners,如果delay的定时器不为空则清空这个定时器,如果超时的定时器不为空,则清空这个定时器

触发超时定时器后,组件请求回来了还会不会执行reslove

不会再次执行,因为resolverejectonce的返回值,once的作用就是保证回调只执行一次

超出loading组件等待时间

如果超出延时时间,并且此时组件还没有加载完成,则将factory.loading设置为true,并执行forceRender方法

// 这个时候传入的 renderCompleted 为 false
const forceRender = (renderCompleted: boolean) => {
  for (let i = 0, l = owners.length; i < l; i++) {
    (owners[i]: any).$forceUpdate()
  }
  if (renderCompleted) {}
}
复制代码

forceRender内触发组件更新,会重新执行resolveAsyncComponent方法,方法内有如下逻辑,会渲染loading组件

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
  return factory.loadingComp
}
复制代码

组件加载成功

这个就和之前的一样了,就是调用reslove函数,获取组件的内容并挂载给factory.resolved上,调用forceRender函数,触发组件更新,更新过程会重新进入resolveAsyncComponent方法,由于factory.resolved有值则直接返回组件内容。组件更新完成后,回到forceRender内,清空所有定时器和变量的值

总结

这三种组件的更新方式首先将使用到当前异步组件的组件实例收集起来,并实现了加载成功和加载失败的回调(resolvereject);然后执行传入的异步函数;当组件加载完成之后会调用成功的回调resolve,成功的回调resolve内遍历收集的所有实例触发它们的$forceUpdate方法重新渲染。

在等待过程中,对于高级异步组件来说可以先使用loading节点,而其他两种则都是注释节点;如果加载失败,高级异步组件会渲染一个错误节点,而其他两种一直是注释节点。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享