为了减少首屏加载时间,按需加载组件 源码 异步组件的公共方法 once 封装 ensureCtor $forceUpdate createAsyncPlaceholder 总结 异步组件的本质是两次渲染,第一次先渲染成占位符(注释节点),等待异步组件获取成功后,再通过 forceRender 强制重新渲染,生成异步加载的组件 0 delay 的高级异步组件第一次直接渲染成 loading 组件

为了减少首屏加载时间,按需加载组件

// 组件的定义不是一个普通对象,所以不会执行Vue.extend的逻辑把它变成一个组件的构造函数
// 但仍然可以执行createComponent函数
// 3种异步组件创建方式
// 方式1 工厂函数形式
Vue.component('async-example', function(resolve, reject) {
  require(['./my-async-component'], function(res) {
    // res就是组件定义
    // 这时候执行resolveAsyncComponent的resolve
    resolve(res);
  });
});
// 方式2 promise形式
Vue.component(
  'async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
);
// 方式3 高级异步形式
const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: '<div>loading</div>',
  // 出错时渲染的组件
  error: '<div>error</div>',
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
});
Vue.component('async-example', AsyncComp);

源码

// src/core/vdom/create-component/js
// 这时候的Ctor是一个函数,所以Ctor.cid是undefined
export function createComponent(Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return;
  }

  const baseCtor = context.$options._base;

  // 当前Ctor不是对象
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // ...

  // Ctor.cid是undefined
  let asyncFactory;
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor;
    // 处理3种异步组件的创建方式(文章一开头写了)
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);
    if (Ctor === undefined) {
      // 异步组件 patch
      // 建一个注释节点作为占位符
      // 然后执行回调函数的resolve
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
    }
  }
}
// src/core/vdom/helpers/resolve-async-component.js
// 处理了3种异步组件的创建方式(文章一开头写了)
export function resolveAsyncComponent(factory: Function, baseCtor: Class<Component>, context: Component): Class<Component> | void {
  // 这个时候,返回factory.errorComp,直接渲染error组件
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp;
  }
  // 这个时候,返回factory.resolved,直接渲染加载成功组件
  if (isDef(factory.resolved)) {
    return factory.resolved;
  }
  // 如果异步组件加载中并未返回,渲染loading组件
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp;
  }
  // 对factory.contexts进行判断,可能多个地方同时初始化一个异步组件
  // 实际只加载一次,所以走到了else
  if (isDef(factory.contexts)) {
    // already pending
    factory.contexts.push(context);
  } else {
    const contexts = (factory.contexts = [context]);
    let sync = true;
    // 定义forceRender
    const forceRender = () => {
      for (let i = 0, l = contexts.length; i < l; i++) {
        // $forceUpdate调用渲染 watcher 的 update 方法,让渲染watcher对应的回调函数执行,触发组件的重新渲染
        // 强制渲染后,context就不是undefined了,那么就会走render和patch的过程,新老node进行响应式渲染
        contexts[i].$forceUpdate();
      }
    };
    // 定义resolve
    // 用once进行封装,保证只执行一次
    const resolve = once((res: Object | Class<Component>) => {
      // 为了找到异步组件js定义的组件对象,如果是一个普通对象,则调用Vue.extend转换成一个组件的构造函数
      // 异步组件加载成功这个就是true,这是高级异步组件的操作,进行强制渲染,又一次执行resolveAsyncComponent
      factory.resolved = ensureCtor(res, baseCtor);
      // ssr的sync为true
      // 当下为false
      if (!sync) {
        // forceRender 会遍历factory.contexts,拿到每一个调用异步组件的实例vm,执行vm.$forceUpdate()
        // $forceUpdate调用渲染 watcher 的 update 方法,让渲染watcher对应的回调函数执行,触发组件的重新渲染
        forceRender();
      }
    });
    // 定义reject
    // 当异步组件加载失败,执行reject函数
    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,这是高级异步组件的操作,进行强制渲染,又一次执行resolveAsyncComponent
        factory.error = true;
        forceRender();
      }
    });
    // 执行组件的定义
    // 工厂方法,res的返回值就是require(['./my-async-component'], resolve);
    // promise方法 res的返回值就是 import('./my-async-component') 的返回值
    // 高级异步组件 res的返回值就是AsyncComp,是一个对象,不是promise,对象里面的components是一个promise
    const res = factory(resolve, reject);

    if (isObject(res)) {
      if (typeof res.then === 'function') {
        // promise异步组件走的位置
        if (isUndef(factory.resolved)) {
          // 当组件异步加载成功后,执行release,失败则执行reject
          res.then(resolve, reject);
        }
      } else if (isDef(res.component) && typeof res.component.then === 'function') {
        res.component.then(resolve, reject);
        // 有没有定义error组件
        if (isDef(res.error)) {
          // 有就赋给factory.errorComp
          // 找到异步组件js定义的组件对象,如果是一个普通对象,则调用Vue.extend转换成一个组件的构造函数
          factory.errorComp = ensureCtor(res.error, baseCtor);
        }
        // 是否定义了loading组件
        if (isDef(res.loading)) {
          // 有就赋给factory.loadingComp
          // 找到异步组件js定义的组件对象,如果是一个普通对象,则调用Vue.extend转换成一个组件的构造函数
          factory.loadingComp = ensureCtor(res.loading, baseCtor);
          // 延迟为0
          if (res.delay === 0) {
            factory.loading = true;
          } else {
            // 延迟delay执行
            setTimeout(() => {
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true;
                forceRender();
              }
            }, res.delay || 200);
          }
        }
        // res.timeout是否存在
        if (isDef(res.timeout)) {
          // 在res.timeout时间后,如果组件没有成功加载,就执行reject
          setTimeout(() => {
            if (isUndef(factory.resolved)) {
              reject(process.env.NODE_ENV !== 'production' ? `timeout (${res.timeout}ms)` : null);
            }
          }, res.timeout);
        }
      }
    }

    sync = false;
    // 如果delay为0,直接渲染loading,否则延迟delay执行forceRender,强制组件渲染,那么又一次执行resolveAsyncComponent
    return factory.loading ? factory.loadingComp : factory.resolved;
  }
}

异步组件的公共方法

  1. once 封装
// src/shared/util.js
// 传入一个函数,返回一个新函数
// 利用闭包和标志位,保证了它包装的函数只执行一次
// 确保resolve和reject只执行一次
export function once(fn: Function): Function {
  let called = false;
  return function() {
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  };
}
  1. ensureCtor
// src/core/vdom/helpers/resolve-async-component.js
// 为了找到异步组件js定义的组件对象,如果是一个普通对象,则调用Vue.extend转换成一个组件的构造函数
function ensureCtor(comp: any, base) {
  if (comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
    comp = comp.default;
  }
  return isObject(comp) ? base.extend(comp) : comp;
}
  1. $forceUpdate
// src / core / instance / lifecycle.js;
// $forceUpdate调用渲染 watcher 的 update 方法,让渲染watcher对应的回调函数执行,触发组件的重新渲染
// 因为Vue是数据驱动视图重新渲染,但是整个异步组件加载过程中没有数据变化的,所以通过$forceUpdate重新渲染一次
Vue.prototype.$forceUpdate = function() {
  const vm: Component = this;
  if (vm._watcher) {
    vm._watcher.update();
  }
};
  1. createAsyncPlaceholder
// src/core/vdom/helpers/resolve-async-components.js
// 创建一个注释vnode,同时把 asyncFactory 和 asyncMeta 赋值给当前 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;
}

总结

异步组件的本质是两次渲染,第一次先渲染成占位符(注释节点),等待异步组件获取成功后,再通过 forceRender 强制重新渲染,生成异步加载的组件

0 delay 的高级异步组件第一次直接渲染成 loading 组件

暂无评论