咱们晓得,组件是Vue体系的外围,纯熟应用组件是把握Vue进行开发的根底。上一节中,咱们深刻理解了Vue组件注册到应用渲染的残缺流程。这一节咱们会在上一节的根底上介绍组件的两个高级用法:异步组件和函数式组件。

6.1 异步组件

6.1.1 应用场景

Vue作为单页面利用遇到最辣手的问题是首屏加载工夫的问题,单页面利用会把页面脚本打包成一个文件,这个文件蕴含着所有业务和非业务的代码,而脚本文件过大也是造成首页渲染速度迟缓的起因。因而作为首屏性能优化的课题,最罕用的解决办法是对文件的拆分和代码的拆散。按需加载的概念也是在这个前提下引入的。咱们往往会把一些非首屏的组件设计成异步组件,局部不影响首次视觉体验的组件也能够设计为异步组件。这个思维就是按需加载。艰深点了解,按需加载的思维让利用在须要应用某个组件时才去申请加载组件代码。咱们借助webpack打包后的后果会更加直观。

webpack遇到异步组件,会将其从主脚本中拆散,缩小脚本体积,放慢首屏加载工夫。当遇到场景须要应用该组件时,才会去加载组件脚本。

6.1.2 工厂函数

Vue中容许用户通过工厂函数的模式定义组件,这个工厂函数会异步解析组件定义,组件须要渲染的时候才会触发该工厂函数,加载后果会进行缓存,以供下一次调用组件时应用。
具体应用:

// 全局注册: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'),  }})
6.1.3 流程剖析

有了上一节组件注册的根底,咱们来剖析异步组件的实现逻辑。简略回顾一下上一节的流程,实例的挂载流程分为依据渲染函数创立Vnode和依据Vnode产生实在节点的过程。期间创立Vnode过程,如果遇到子的占位符节点会调用creatComponent,这里会为子组件做选项合并和钩子挂载的操作,并创立一个以vue-component-为标记的子Vnode,而异步组件的解决逻辑也是在这个阶段解决。

// 创立子组件过程  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  }

工厂函数的用法使得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. 定义异步申请胜利的函数解决,定义异步申请失败的函数解决;
    1. 执行组件定义的工厂函数;
    1. 同步返回申请胜利的函数解决。

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定义的办法,办法会提醒并标记谬误,最初同样会强制更新视图。参考Vue3源码视频解说:进入学习

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

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

createAsyncPlaceholder的定义也很简略,其中createEmptyVNode之前有介绍过,是创立一个正文节点vnode,而asyncFactory,asyncMeta都是用来标注该节点为异步组件的长期节点和相干属性。

// 创立正文Vnodefunction 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过程。这时,旧的正文节点也会被取代。

6.1.4 Promise异步组件

异步组件的第二种写法是在工厂函数中返回一个promise对象,咱们晓得importes6引入模块加载的用法,然而import是一个动态加载的办法,它会优先模块内的其余语句执行。因而引入了import(),import()是一个运行时加载模块的办法,能够用来类比require()办法,区别在于前者是一个异步办法,后者是同步的,且import()会返回一个promise对象。

具体用法:

Vue.component('asyncComponent', () => import('./test.vue'))

源码仍然走着异步组件解决分支,并且大部分的处理过程还是工厂函数的逻辑解决,区别在于执行异步函数后会返回一个promise对象,胜利加载则执行resolve,失败加载则执行reject.

var res = factory(resolve, reject);// res是返回的promiseif (isObject(res)) {  if (isPromise(res)) {    if (isUndef(factory.resolved)) {      // 外围解决      res.then(resolve, reject);    }  }}

其中promise对象的判断最简略的是判断是否有thencatch办法:

 // 判断promise对象的办法  function isPromise (val) {    return (isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function')  }
6.1.5 高级异步组件

为了在操作上更加灵便,比方应用loading组件解决组件加载工夫过长的期待问题,应用error组件解决加载组件失败的谬误提醒等,Vue在2.3.0+版本新增了返回对象模式的异步组件格局,对象中能够定义须要加载的组件component,加载中显示的组件loading,加载失败的组件error,以及各种延时超时设置,源码同样进入异步组件分支。

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

异步组件函数执行后返回一个对象,并且对象的component执行会返回一个promise对象,因而进入高级异步组件解决分支。

if (isObject(res)) {  if (isPromise(res)) {}  // 返回对象,且res.component返回一个promise对象,进入分支  // 高级异步组件解决分支  else if (isPromise(res.component)) {    // 和promise异步组件解决形式雷同    res.component.then(resolve, reject);    ···  }}

异步组件会期待响应成功失败的后果,与此同时,代码持续同步执行。高级选项设置中如果设置了errorloading组件,会同时创立两个子类的结构器,

if (isDef(res.error)) {  // 异步谬误时组件的解决,创立谬误组件的子类结构器,并赋值给errorComp  factory.errorComp = ensureCtor(res.error, baseCtor);}if (isDef(res.loading)) {  // 异步加载时组件的解决,创立谬误组件的子类结构器,并赋值给errorComp  factory.loadingComp = ensureCtor(res.loading, baseCtor);}

如果存在delay属性,则通过settimeout设置loading组件显示的延迟时间。factory.loading属性用来标注是否是显示loading组件。

if (res.delay === 0) {  factory.loading = true;} else {  // 超过工夫会胜利加载,则执行失败后果  setTimeout(function () {    if (isUndef(factory.resolved) && isUndef(factory.error)) {      factory.loading = true;      forceRender(false);    }  }, res.delay || 200);}

如果在timeout工夫内,异步组件还未执行resolve的胜利后果,即resolve没有赋值,则进行reject失败解决。

接下来仍然是渲染正文节点或者渲染loading组件,期待异步处理结果,依据处理结果从新渲染视图节点,类似过程不再论述。

6.1.6 wepack异步组件用法

webpack作为Vue利用构建工具的标配,咱们须要晓得Vue如何联合webpack进行异步组件的代码拆散,并且须要关注拆散后的文件名,这个名字在webpack中称为chunkNamewebpack为异步组件的加载提供了两种写法。

  • require.ensure:它是webpack传统提供给异步组件的写法,在编译时,webpack会动态地解析代码中的 require.ensure(),同时将模块增加到一个离开的 chunk 中,其中函数的第三个参数为拆散代码块的名字。批改后的代码写法如下:
Vue.component('asyncComponent', function (resolve, reject) {   require.ensure([], function () {     resolve(require('./test.vue'));   }, 'asyncComponent'); // asyncComponent为chunkname})
  • import(/* webpackChunkName: "asyncComponent" */, component): 有了es6,import的写法是现今官网最举荐的做法,其中通过正文webpackChunkName来指定拆散后组件模块的命名。批改后的写法如下:
Vue.component('asyncComponent', () => import(/* webpackChunkName: "asyncComponent" */, './test.vue'))

至此,咱们曾经把握了所有异步组件的写法,并深刻理解了其外部的实现细节。我置信全面的把握异步组件对今后单页面性能优化方面会起到踊跃的指导作用。

6.2 函数式组件

Vue提供了一种能够让组件变为无状态、无实例的函数化组件。从原理上说,个别子组件都会通过实例化的过程,而单纯的函数组件并没有这个过程,它能够简略了解为一个中间层,只解决数据,不创立实例,也是因为这个行为,它的渲染开销会低很多。理论的利用场景是,当咱们须要在多个组件中抉择一个来代为渲染,或者在将children,props,data等数据传递给子组件前进行数据处理时,咱们都能够用函数式组件来实现,它实质上也是对组件的一个内部包装。

6.2.1 应用场景

  • 定义两个组件对象,test1,test2
var test1 = {  props: ['msg'],  render: function (createElement, context) {    return createElement('h1', this.msg)  }}var test2 = {  props: ['msg'],  render: function (createElement, context) {    return createElement('h2', this.msg)  }}
  • 定义一个函数式组件,它会依据计算结果抉择其中一个组件进行选项
Vue.component('test3', {  // 函数式组件的标记 functional设置为true  functional: true,  props: ['msg'],  render: function (createElement, context) {    var get = function() {      return test1    }    return createElement(get(), context)  }})
  • 函数式组件的应用
<test3 :msg="msg" id="test"></test3>new Vue({  el: '#app',  data: {    msg: 'test'  }})
  • 最终渲染的后果为:
<h2>test</h2>

6.2.2 源码剖析

函数式组件会在组件的对象定义中,将functional属性设置为true,这个属性是区别一般组件和函数式组件的要害。同样的在遇到子组件占位符时,会进入createComponent进行子组件Vnode的创立。因为functional属性的存在,代码会进入函数式组件的分支中,并返回createFunctionalComponent调用的后果。留神,执行完createFunctionalComponent后,后续创立子Vnode的逻辑不会执行,这也是之后在创立实在节点过程中不会有子Vnode去实例化子组件的起因。(无实例)

function createComponent(){  ···  if (isTrue(Ctor.options.functional)) {    return createFunctionalComponent(Ctor, propsData, data, context, children)  }}

createFunctionalComponent办法会对传入的数据进行检测和合并,实例化FunctionalRenderContext,最终调用函数式组件自定义的render办法执行渲染过程。

function createFunctionalComponent(  Ctor, // 函数式组件结构器  propsData, // 传入组件的props  data, // 占位符组件传入的attr属性  context, // vue实例  children// 子节点){  // 数据检测合并  var options = Ctor.options;  var props = {};  var propOptions = options.props;  if (isDef(propOptions)) {    for (var key in propOptions) {      props[key] = validateProp(key, propOptions, propsData || emptyObject);    }  } else {    // 合并attrs    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }    // 合并props    if (isDef(data.props)) { mergeProps(props, data.props); }  }  var renderContext = new FunctionalRenderContext(data,props,children,contextVm,Ctor);  // 调用函数式组件中自定的render函数  var vnode = options.render.call(null, renderContext._c, renderContext)}

FunctionalRenderContext这个类最终的目标是定义一个和实在组件渲染不同的render办法。

function FunctionalRenderContext() {  // 省略其余逻辑  this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };}

执行render函数的过程,又会递归调用createElement的办法,这时的组件曾经是实在的组件,开始执行失常的组件挂载流程。

问题:为什么函数式组件须要定义一个不同的createElement办法?- 函数式组件createElement和以往惟一的不同是,最初一个参数的不同,之前章节有说到,createElement会依据最初一个参数决定是否对子Vnode进行拍平,个别状况下,children编译生成后果都是Vnode类型,只有函数式组件比拟非凡,它能够返回一个数组,这时候拍平就是有必要的。咱们看上面的例子:

Vue.component('test', {    functional: true,    render: function (createElement, context) {      return context.slots().default    }  }) <test>      <p>slot1</p>      <p>slot</p> </test>

此时函数式组件testrender函数返回的是两个slotVnode,它是以数组的模式存在的,这就是须要拍平的场景。

简略总结一下函数式组件,从源码中能够看出,函数式组件并不会像一般组件那样有实例化组件的过程,因而包含组件的生命周期,组件的数据管理这些过程都没有,它只会一成不变的接管传递给组件的数据做解决,并渲染须要的内容。因而作为纯正的函数能够也大大降低渲染的开销。

6.3 小结

这一大节在组件根底之上介绍了两个进阶的用法,异步组件和函数式组件。它们都是为了解决某些类型场景引入的高级组件用法。其中异步组件是首屏性能优化的一个解决方案,并且Vue提供了多达三种的应用办法,高级配置的用法更让异步组件的应用更加灵便。当然大部分状况下,咱们会联合webpack进行应用。另外,函数式组件在多组件中抉择渲染内容的场景作用不凡,因为是一个无实例的组件,它在渲染开销上比一般组件的性能更好。