vue2从数据到视图渲染:异步组件

1,232 阅读5分钟

Vue大多用在单页面应用中,一个页面只有一个<div id="app"></div>承载所有节点,因此复杂项目可能会出现首屏加载白屏等问题,Vue异步组件就很好的处理了这问题。。

一、异步组件渲染主流程

1、Vue.component注册

首先执行Vue.component进行组件的注册:

ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          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
      }
    }
  })

其中isPlainObject(definition)false,最终执行到this.options[type + 's'][id] = definition,没有像同步组件一样对异步组件处理成构造函数,此时的definition还是原始数据。

2、createComponent函数

组件实例化vNode的详细过程请移步juejin.cn/post/712909… 这里只记录异步组件不一样的地方,在异步组件中,并没有执行逻辑 Ctor = baseCtor.extend(Ctor),所以Ctor.cid未定义,进入以下逻辑:

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

asyncFactory就是当前的CtorbaseCtor就是Vue,执行解析异步组件的方法Ctor = resolveAsyncComponent(asyncFactory, baseCtor):

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) {
    // already pending
    factory.owners.push(owner)
  }

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

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

这个过程中,有以下主要流程:

(1)定义 owners

_render刚开始执行的时候先定义currentRenderingInstance = vm,然后再当前方法中var owner = currentRenderingInstancevar owners = factory.owners = [owner]共同决定了,factory.owners就是一个vm实例。

(2)定义resolve

resolve中主要实现了函数只执行一次,将对象转换成组件构造函数和进行视图的强制渲染。

/**
 * 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);
    }
  };
}

通过once进行包裹确保只会执行一次resolve,一个函数只执行一次的once是通过闭包中控制called变量来实现的。

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

确保异步执行结束后,返回的结果如果是普通对象,则将其通过Vue上的方法转换成构造函数。

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

该方法是等异步执行结束后,owners执行$forceUpdate()函数:

Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

会执行Watcher实例的update方法,进行视图的重新渲染。

(3)定义reject

同样用once进行包裹,如果定义了factory.errorComp还会执行factory.error = true;forceRender(true)

(4)执行factory

当前方法const res = factory(resolve, reject)如果异步执行,会推到异步队列中等待同步执行完毕后执行其中的resolve函数;如果是同步函数,则会返回res。

(5)判断res是否存在

如果const res = factory(resolve, reject)中返回res,并且res.componentPromise,就会执行res.component.then(resolve, reject),在.then方法中执行resolve回调。在整个过程中还根据res中的属性,还做了以下几件事:

  • 如果res.error存在,通过factory.errorComp = ensureCtor(res.error, baseCtor)获取错误时的组件构造函数
  • 如果res.loading存在,通过factory.loadingComp = ensureCtor(res.loading, baseCtor)获取加载时的构造函数。如果延时res.delay === 0factory.loading = true,并且在解析异步组件结束时return factory.loading ? factory.loadingComp : factory.resolved,直接返回factory.loadingComp作为组件构造函数,在后续的渲染中直接渲染加载组件;如果有延时,则定义一个定时器,在定时器结束时组件既没有失败也没有成功,则进行加载组件的渲染。
  • 如果res.timeout存在,则定义一个定时器,在时间res.timeoutfactory.resolved依然不存在,说明组件没有完成引入,则执行reject,进而执行失败组件的渲染。
(6) 返回组件构造函数

sync = false将异步变量置为false,并通过return factory.loading ? factory.loadingComp : factory.resolved返回需要渲染的组件构造函数。

二、例子

异步组件的引入有vue.js官网推荐的如下方式

1、require引入

Vue.component("async-Component-1", function(resolve, reject) {
  setTimeout(() => {
    require(["./components/asyn-component-1.vue"], function(res) {
      resolve(res);
    });
  }, 1000);
});

在当前例子中会等待引入async-Component-1后执行成功后视图的渲染,ensureCtor将组件对象转换成构造函数,然后forceRender(true)强制渲染视图。

2、import引入

把webpack 2 和 ES2015 语法加在一起,可以使用动态导入

Vue.component("async-Component-2", () =>
  import("./components/asyn-component-2.vue")
);

3、处理加载状态

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

这个例子中失败时会渲染组件ErrorComponent,否则,怎么判定先执行LoadingComponent还是执行import('./MyComponent.vue')? 如果,import('./MyComponent.vue')先引入加载完成,则先执行res.component.then(resolve, reject)中的resolve,进而渲染成功后的组件;如果

timerLoading = setTimeout(() => {
  timerLoading = null
  if (isUndef(factory.resolved) && isUndef(factory.error)) {
    factory.loading = true
    forceRender(false)
  }
}, res.delay || 200)

先执行,那么会先通过forceRender(false)去进行加载组件的渲染,这里的false表示不会进行当前关联实例owners的清空和当前timerLoading的终止,等待res.component.then(resolve, reject)执行resolve的逻辑。
这里主要的是,Promise异步res.component.then(resolve, reject)和定时器timerLoading争取执行权,谁跑到前面先执行谁。

小结

异步组件是首屏加载优化和骨架屏实现的方案之一,同时,异步组件的思路也可以通过权限管理来控制功能组件的渲染的权限。