Vue组件高级用法之异步组件(上)

2,064 阅读5分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

背景:Vue作为单页面应用最先遇到的问题是首屏加载时间的问题.单页面应用会把页面脚本打包成一个文件,这个文件包含着所有业务和非业务的代码,而脚本文件过大也是造成首页渲染速度缓慢的原因。因此作为首屏性能优化的课题,最常用的处理方法是对文件的拆分和代码的分离。按需加载的概念也是在这个前提下引入的。 以下是webpack打包后呈现的结果对比:

image.png

image.png webpack遇到异步组件,会将其从主脚本中分离,减少脚本体积,加快首屏加载时间。当遇到场景需要使用该组件时,才会去加载组件脚本。

工厂函数

// 全局注册:
Vue.component('asyncComponent', function(resolve, reject) {
  require(['./test.vue'], resolve)
})
// 局部注册:
var vm = new Vue({
  el: '#app',
  template: '<div id="app"><asyncComponent></asyncComponent></div>',
  components: {
    asyncComponent: (resolve, reject) => require(['./test.vue'], resolve),
    // 另外写法
    asyncComponent: () => import('./test.vue'),
  }
})

接下来,我们来分析异步组件的实现逻辑。依托一下代码为例:

// 创建子组件过程
  function createComponent (
    Ctor, // 子类构造器
    data,
    context, // vm实例
    children, // 子节点
    tag // 子组件占位符
  ) {
    ···
    // 针对局部注册组件创建子类构造器
    if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
    }
    // 异步组件分支
    var asyncFactory;
    if (isUndef(Ctor.cid)) {
      // 异步工厂函数
      asyncFactory = Ctor;
      // 创建异步组件函数
      Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
      if (Ctor === undefined) {
        return createAsyncPlaceholder(
          asyncFactory,
          data,
          context,
          children,
          tag
        )
      }
    }
    ···
    // 创建子组件vnode
    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
      asyncFactory
    );

    return vnode
  }

实例的挂载流程分为根据渲染函数创建Vnode和根据Vnode产生真实节点的过程。期间创建Vnode过程,如果遇到子的占位符节点会调用creatComponent,这里会为子组件做选项合并和钩子挂载的操作,并创建一个以vue-component-为标记的子Vnode,而异步组件的处理逻辑也是在这个阶段处理。 工厂函数的用法使得Vue.component(name, options)的第二个参数不是一个对象,因此不论是全局注册还是局部注册,都不会执行Vue.extend生成一个子组件的构造器, 所以Ctor.cid不会存在,代码会进入异步组件的分支。

异步组件分支的核心是resolveAsyncComponent,它的处理逻辑分支众多,我们先关心工厂函数处理部分。

function resolveAsyncComponent (
    factory,
    baseCtor
  ) {
    if (!isDef(factory.owners)) {

      // 异步请求成功处理
      var resolve = function() {}
      // 异步请求失败处理
      var reject = function() {}

      // 创建子组件时会先执行工厂函数,并将resolve和reject传入
      var res = factory(resolve, reject);

      // resolved 同步返回
      return factory.loading
        ? factory.loadingComp
        : factory.resolved
    }
  }

如果经常使用promise进行开发,我们很容易发现,这部分代码像极了promsie原理内部的实现,针对异步组件工厂函数的写法,大致可以总结出以下三个步骤:

  1. 定义异步请求成功的函数处理,定义异步请求失败的函数处理;
  2. 执行组件定义的工厂函数;
  3. 同步返回请求成功的函数处理。

resolve, reject的实现,都是once方法执行的结果,所以我们先关注一下高级函数once的原理。为了防止当多个地方调用异步组件时,resolve,reject不会重复执行,once函数保证了函数在代码只执行一次。也就是说,once缓存了已经请求过的异步组件

// once函数保证了这个调用函数只在系统中调用一次
function once (fn) {
  // 利用闭包特性将called作为标志位
  var called = false;
  return function () {
    // 调用过则不再调用
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  }
}

成功resolve和失败reject的详细处理逻辑如下:

// 成功处理
var resolve = once(function (res) {
  // 转成组件构造器,并将其缓存到resolved属性中。
  factory.resolved = ensureCtor(res, baseCtor);
  if (!sync) {
    //强制更新渲染视图
    forceRender(true);
  } else {
    owners.length = 0;
  }
});
// 失败处理
var reject = once(function (reason) {
  warn(
    "Failed to resolve async component: " + (String(factory)) +
    (reason ? ("\nReason: " + reason) : '')
  );
  if (isDef(factory.errorComp)) {
    factory.error = true;
    forceRender(true);
  }
});

异步组件加载完毕,会调用resolve定义的方法,方法会通过ensureCtor将加载完成的组件转换为组件构造器,并存储在resolved属性中,其中 ensureCtor的定义为:

function ensureCtor (comp, base) {
    if (comp.__esModule ||(hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
      comp = comp.default;
    }
    // comp结果为对象时,调用extend方法创建一个子类构造器
    return isObject(comp)
      ? base.extend(comp)
      : comp
  }

组件构造器创建完毕,会进行一次视图的重新渲染,由于Vue是数据驱动视图渲染的,而组件在加载到完毕的过程中,并没有数据发生变化,因此需要手动强制更新视图。 forceRender函数的内部会拿到每个调用异步组件的实例,执行原型上的$forceUpdate方法,这部分的知识等到响应式系统时介绍。

异步组件加载失败后,会调用reject定义的方法,方法会提示并标记错误,最后同样会强制更新视图。

回到异步组件创建的流程,执行异步过程会同步为加载中的异步组件创建一个注释节点Vnode

  function createComponent (){
    ···
    // 创建异步组件函数
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    if (Ctor === undefined) {
      // 创建注释节点
      return createAsyncPlaceholder(asyncFactory,data,context,children,tag)
    }
  }

createAsyncPlaceholder的定义也很简单,其中createEmptyVNode之前有介绍过,是创建一个注释节点vnode,而asyncFactory,asyncMeta都是用来标注该节点为异步组件的临时节点和相关属性。

// 创建注释Vnode
function createAsyncPlaceholder (factory,data,context,children,tag) {
  var node = createEmptyVNode();
  node.asyncFactory = factory;
  node.asyncMeta = { data: data, context: context, children: children, tag: tag };
  return node
}

执行forceRender触发组件的重新渲染过程时,又会再次调用resolveAsyncComponent,这时返回值Ctor不再为 undefined了,因此会正常走组件的render,patch过程。这时,旧的注释节点也会被取代。