咱们晓得,组件是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
,而异步组件的解决逻辑也是在这个阶段解决。参考Vue3源码视频解说:进入学习
// 创立子组件过程 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
原理外部的实现,针对异步组件工厂函数的写法,大抵能够总结出以下三个步骤:
- 定义异步申请胜利的函数解决,定义异步申请失败的函数解决;
- 执行组件定义的工厂函数;
- 同步返回申请胜利的函数解决。
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
都是用来标注该节点为异步组件的长期节点和相干属性。
// 创立正文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
对象,咱们晓得import
是es6
引入模块加载的用法,然而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
对象的判断最简略的是判断是否有then
和catch
办法:
// 判断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); ··· }}
异步组件会期待响应成功失败的后果,与此同时,代码持续同步执行。高级选项设置中如果设置了error
和loading
组件,会同时创立两个子类的结构器,
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
中称为chunkName
。webpack
为异步组件的加载提供了两种写法。
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>
此时函数式组件test
的render
函数返回的是两个slot
的Vnode
,它是以数组的模式存在的,这就是须要拍平的场景。
简略总结一下函数式组件,从源码中能够看出,函数式组件并不会像一般组件那样有实例化组件的过程,因而包含组件的生命周期,组件的数据管理这些过程都没有,它只会一成不变的接管传递给组件的数据做解决,并渲染须要的内容。因而作为纯正的函数能够也大大降低渲染的开销。
6.3 小结
这一大节在组件根底之上介绍了两个进阶的用法,异步组件和函数式组件。它们都是为了解决某些类型场景引入的高级组件用法。其中异步组件是首屏性能优化的一个解决方案,并且Vue
提供了多达三种的应用办法,高级配置的用法更让异步组件的应用更加灵便。当然大部分状况下,咱们会联合webpack
进行应用。另外,函数式组件在多组件中抉择渲染内容的场景作用不凡,因为是一个无实例的组件,它在渲染开销上比一般组件的性能更好。