共计 9751 个字符,预计需要花费 25 分钟才能阅读完成。
咱们晓得,组件是
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
都是用来标注该节点为异步组件的长期节点和相干属性。
// 创立正文 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
过程。这时,旧的正文节点也会被取代。
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 是返回的 promise
if (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
进行应用。另外,函数式组件在多组件中抉择渲染内容的场景作用不凡,因为是一个无实例的组件,它在渲染开销上比一般组件的性能更好。