使用过一段时间 class 来定义组件,要用 vue-property-decorator 提供定义好的装饰器,辅助完成所需功能,对这个过程好奇,就研究了源码。内部主要依靠 vue-class-component 实现,因此将重点放在对 vue-class-component 的解读上。javascript
本文主要内容有:前端
没有使用 class 方式定义组件时,一般导出一个选项对象:vue
<script> export default { props: { name: String }, data() { return { message: '新消息' } }, watch: { message(){ console.log('message改变触发') } }, computed:{ hello: { get(){ return this.message + 'hello'; }, set(newValue){} } }, methods:{ clickHandler(){} } mounted(){ console.log('挂载完毕'); } } </script>
这个对象告诉 Vue 你要作什么事情,须要哪些功能。 根据字段的不一样做用,把须要添加的属性和方法,写在指定的位置,例如,须要响应式数据写在 data 中、计算属性写在 computed 中、事件函数写在 methods中、直接写生命周期函数等 。Vue 内部会调用 Vue.extend() 建立组件的构造函数,以便在模板中使用时,经过构造函数初始化此组件。java
若是使用了 class 来定义组件,上面的字段可省略,但要符合 Vue 内部使用数据的规则,就须要重组这些数据。git
定义 class 组件:github
<script lang="ts"> class Home extends Vue { message = '新数据'; get hello(){ return this.message + 'hello'; } set hello(newValue){} clickHandler(){} mounted(){} } Home.prototype.age = '年龄' </script>
message 做为响应式的数据,应该放在 data 中,但问题是 message 写在类中,为初始化后实例上的属性,就要想办法在初始化后拿到 message,放在 data 中。app
age 直接写在原型上,值不是函数,也应该放在 data 中。函数
hello 写了访问器,做为计算属性,写在 computed 中;clickHandler做为方法,写在 methods 中;mounted 是生命周期函数,挂载原型上就能够,不须要动。这三个都是方法,定义在原型上,须要拿到原型对象,找到这三类方法,按照特性放在指定位置。工具
这就引起一个问题,怎么把这些定义的属性放在 Vue 须要解析的数据中,“上帝的归上帝,凯撒的归凯撒”。学习
最终处理成这样:
{ data:{ message: '新数据', age: '年龄' }, methods:{ clickHandler(){} }, computed:{ hello:{ get(){ return this.message + 'hello'; } } }, mounted(){} }
最好是无入侵式的添加功能,开发者无感知,正常写业务代码,提供封装好功能来完成归类数据这件事。
装饰器模式,在不改变自身对象的基础上,动态增长额外的功能,这个模式的思路符合上述内容的要求。具体可参考一篇文章详细了解,装饰者模式和TypeScript装饰器
vue-class-component 的代码使用 ts 书写,若是对 ts 语法不熟悉,能够忽略定义的类型,直接看函数体内的逻辑,不影响阅读。或者直接看打包后,没有压缩的代码,也很少,大约200行左右。
本文分析的代码主要文件在:仓库地址
先来看大体结构和如何使用:
function Component(options) { // options 是 function类型,是要装饰的类 if (typeof options === 'function') { return componentFactory(options); } // 执行后,这个函数做为装饰器函数,接收要装饰的类 // options 为传入的选项数据。 return function (Component) { return componentFactory(Component, options); }; } // 使用1 @Component class Home Extend Vue {} // 使用2 @Component({ components:{} data:{newMessage: '增长的消息'}, methods:{ moveHandler(){} }, computed:{ reveserMessage(){ return this.newMessage + '翻转' } } // ... vue中选项对象其余值 }) class Home Extend Vue {}
Component 做为装饰器函数,接受的 options 就是要装饰的类 Home, js 中类不过是一种语法糖,typeof Home 获得为 function 类型。
Component 函数做为工厂函数,执行并传入参数 options(为了称呼方便,后面把这个参数叫作 装饰器选项数据),工厂函数执行后,返回装饰器函数,一样是接受要装饰的类 Home。
从代码中能够看出来,都调用了 componentFactory ,第一个参数为要装饰的类,第二参数可选,传入的话就是装饰器选项数据。
从名字上能够看出来,componentFactory 用来产生组件的工厂,通过一系列的执行后,返回新的组件函数。省略其余,先看关键代码 代码地址:
function componentFactory(Component) { // 省略其余代码... // 参数为两个,说明第二个是传入的部分选项数据; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // 获得继承的父类,不出意外为 Vue var superProto = Object.getPrototypeOf(Component.prototype); // 若是原型链上确实有 Vue,则获得构造函数;不为 Vue,则直接使用 Vue; // 目的是为了找到 extend 函数。 var Super = superProto instanceof Vue ? superProto.constructor : Vue; // 根据选项对象,新建一个组件的构造函数 var Extended = Super.extend(options); // 返回新的构造函数 return Extended; }
验证了上面的猜想,调用了 Vue.extend 返回新的组件函数。但在返回以前,要处理原来组件上的属性,和原型上的方法。
首先对选项上的方法归类,方法归 methods;非方法归 data;有访问器归 computed。
// 须要忽略的属性 const $internalHooks = [ 'data', 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeDestroy', 'destroyed', 'beforeUpdate', 'updated', 'activated', 'deactivated', 'render', 'errorCaptured', // 2.5 'serverPrefetch' // 2.6 ] function componentFactory(Component) { // 其余代码省略... // 拿到原型对象 const proto = Component.prototype // 返回对象上全部自身属性,包括不可枚举的属性 Object.getOwnPropertyNames(proto).forEach(function (key) { // 构造函数,不作处理 if (key === 'constructor') { return } // 钩子函数之类的属性,直接赋值到 options对象上,不须要归类 if ($internalHooks.indexOf(key) > -1) { options[key] = proto[key] return } // 拿到对应属性的描述对象,用这个方法能避免继续查找原型链上的属性 const descriptor = Object.getOwnPropertyDescriptor(proto, key); // 若是此属性的值不为 undefined,说明有值 if (descriptor.value !== void 0) { // methods // 若是为函数,则直接归为 methods if (typeof descriptor.value === 'function') { (options.methods || (options.methods = {}))[key] = descriptor.value } else { // 若是值不为函数,则归为data,这里采用 mixins,混合数据的方式来作 (options.mixins || (options.mixins = [])).push({ data (this: Vue) { return { [key]: descriptor.value } } }) } } else if (descriptor.get || descriptor.set) { // value 为空,可是有 get或set的访问器,则归为computed (options.computed || (options.computed = {}))[key] = { get: descriptor.get, set: descriptor.set } } }) }
从上述代码能够看出来,拿到属性对应的描述对象,根据属性对应的值,进行类型判断,来决定归为哪一类。
值得注意的是这段代码,目的是把非函数的属性,混合在 data 中:
if(typeof descriptor.value === 'function'){/*省略*/} else{// 处理原型上不是函数的状况 (options.mixins || (options.mixins = [])).push({ data (this: Vue) { return { [key]: descriptor.value } } }) }
通常写在类中的只有是函数才能放在原型上,但有别的方式能够把非函数的值添加到原型上:
// 第一种,直接给原型添加属性 Home.prototype.age = 18; // 第二种,用属性装饰器 function ageDecorator(prototype, key){ return { // 装饰器返回描述对象,会在 prototype增长key这个属性 enumerable: false, value: 18 } } class Home extends Vue { @ageDecorator age: number = 18; }
若是用了 ts 的属性装饰器,并返回描述对象,就会在 prototype 增长这个属性,因此在上面 componentFactory 源码中要处理这种状况,通常在项目中比较少见。
写在类中的属性,不添加在原型上,只有经过获得实例后拿到这些值,能够沿着这个思路进行分析。
先看实例上属性的状况:
class Home { message: '新消息', clickHandler(){} } let home = new Home(); console.log(home); // 打印实例,简化后: { message: "新消息" __proto__: constructor: class Home clickHandler: ƒ clickHandler() __proto__: Object }
在 componentFactory 中作了单独的处理:
function componentFactory(Component){ // 省略其余代码 ;(options.mixins || (options.mixins = [])).push({ data () { return collectDataFromConstructor(this, Component) } }) }
这里依然使用混合 data 的方式,混合功能很强大,敲黑板记下来。mixins 会在初始化组件时,调用 data 对应的函数,获得要混合的数据,又调用了 collectDataFromConstructor,传入 this,为组件实例,跟平时写项目在 mounted 中使用的那个 this 同样,都为渲染组件的实例;第二参数为 Component,是原来装饰的类,上面例子中就是 Home 类。
这个函数的目的是把原来装饰的类,初始以后,拿到实例上的属性组成对象返回。代码地址
来看代码:
// 用来收集被装饰类中定义的属性 // vm 为要渲染的组件实例 // Component 为原来要装饰的组件类 function collectDataFromConstructor(vm, Component) { // 先保存原有的 _init,目的是不执行 Vue上的 _init 作其余初始化动做 var originalInit = Component.prototype._init; // 在被装饰的类的原型上手动增长 _init,在Vue实例化事内部会调用 Component.prototype._init = function () { var _this = this; // 拿到渲染组件对象上的属性,包括不可枚举的属性,包含组件内定义的 $开头属性 和 _开头属性,还有自定义的一些方法 var keys = Object.getOwnPropertyNames(vm); // 若是渲染组件含有,props,可是并无放在原组件实例上,则添加上 if (vm.$options.props) { for (var key in vm.$options.props) { if (!vm.hasOwnProperty(key)) { keys.push(key); } } } // 把给原组件实例上 Vue 内置属性设置为不可遍历。 keys.forEach(function (key) { if (key.charAt(0) !== '_') { Object.defineProperty(_this, key, { get: function get() { return vm[key]; }, set: function set(value) { vm[key] = value; }, configurable: true }); } }); }; // 手动初始化要包装的类,目的是拿到初始化后实例 var data = new Component(); // 从新还原回原来的 _init,防止一直引用原有的实例,形成内存泄漏 Component.prototype._init = originalInit; // 从新定义对象 var plainData = {}; // Object.keys 拿到可被枚举的属性,添加到对象中 Object.keys(data).forEach(function (key) { if (data[key] !== undefined) { plainData[key] = data[key]; } }); return plainData; }
具体要作的话,经过 new Component() 获得被装饰类的实例,但要注意,Component 继承了 Vue 类,初始化后实例上有不少 Vue 内部添加上的属性,好比 $options、$parent、$attrs、$listeners、$data 等等,还有以 _ 开头的属性,_watcher、_renderProxy 等等,还有咱们须要的属性。这里只是简单举几个属性,你能够手动初始化,在控制台打印输出看一下。
以 _ 开头的属性,是内置方法,不可被枚举;以 $ 开头的属性,也是内置方法,可是可被枚举。若是直接循环实例,会拿到以 $ 开头的属性,这并非咱们须要的。
那怎么办呢?代码中给了答案,在初始化一系列组件内置的属性后,组件内部会调用 Component.prototype._init 方法,可经过改写这个方法,来处理属性为不可枚举。
最后经过 Object.keys() 获得可以被遍历的属性。
上面拐的弯比较多,不免看蒙了,根据核心意思,简化以下:
原来有个组件:
class Home { message: '新消息' }
如今有个须要渲染的组件,要把上面定义在 Home 中的 message 写在现有组件的 data 中:
const App = Vue.extend({ // 混合功能 mixins:[{ data(){ // 初始化后拿到实例,就能拿到 message 属性 let data = new Home(); let plainData = {}; Object.keys(data).forEach(function (key) { if (data[key] !== undefined) { plainData[key] = data[key]; } }); return plainData; } }], data(){ return { other: '其余data' } } }) new App().$mounted('#app');
简化后,是否是清晰不少,本质就是初始类获得实例,拿属性组成对象,混合到渲染的组件中。
小的优化点,简化代码:
// 保留原有的 _init 方法 var originalInit = Component.prototype._init; Component.prototype._init = function(){ // 其余代码省略 }; Component.prototype._init = originalInit;
这段代码,在改写的 _init 内部使用了外面的引用 vm 和 Component,就会一直在内存中,为防止内存泄漏,从新赋回原来的函数。
vue-property-decorator 依赖 vue-class-component 实现,主要用了内部提供的 createDecorator 方法。
若是你想增长更多装饰器,也能够经过调用 createDecorator 方法,原理很简单,就是向选项对象上增长所需数据。
在 vue-class-component 中提供了工具函数 createDecorator 容许添加其余额外的装饰函数,统一挂载在 Component.__decorators__ 上,并把 options 传过去,对 options 增长须要的属性,实际上会调用这些装饰函数,让这些函数有机会处理 options。
function componentFactory(Component) { // 省略其余代码.... var decorators = Component.__decorators__; if (decorators) { decorators.forEach(function (fn) { return fn(options); }); delete Component.__decorators__; } }
咱们能够利用 createDecorator,扩展其余的装饰器,vue-property-decorator 内部就是利用这个函数扩展了 @Prop、@Watch 等装饰器。
function createDecorator(factory) { return (target, key, index) => { // 是函数类型,则为装饰的类; // 不然,为原型,经过constructor拿到构造函数 const Ctor = typeof target === 'function' ? target : target.constructor; if (!Ctor.__decorators__) { Ctor.__decorators__ = []; } // 当为参数装饰器时,index为number if (typeof index !== 'number') { index = undefined; } Ctor.__decorators__.push(options => factory(options, key, index)); }; }s
从源码中能够看出来,createDecorator 调用后会返回一个函数,这个函数能够做为装饰器函数,接收的 target 若是是函数类型,说明做为类装饰器,target 就是被装饰的类;不然,获得的是原型,经过 constructor 拿到构造函数。
向要装饰的类上添加静态属性 decorators,存入一个函数,得到 options。
如今来看 vue-property-decorator 中 watch 装饰器的源码,代码地址
function Watch(path, options) { if (options === void 0) { options = {}; } return createDecorator(function (componentOptions, handler) { if (typeof componentOptions.watch !== 'object') { componentOptions.watch = Object.create(null); } var watch = componentOptions.watch; if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) { watch[path] = [watch[path]]; } else if (typeof watch[path] === 'undefined') { watch[path] = []; } watch[path].push({ handler: handler}); }); }
传入 createDecorator 的回调函数,会接受两个参数,componentOptions 为一个对象,就是在上面 componentFactory 中调用 Component.__decorators__,传入的对象,目的是向这个对象添加或增长 watch 属性,给要装饰的类使用;handler 是函数名字;
这样使用:
@Component class Home extend Vue { message='新消息' @watch('message') messageHandler(){ console.log('当message改变后,执行这里') } }
通过 @watch 装饰器处理后,选项对象上会增长一段数据:
{ watch: { message: 'messageHandler' }, methods:{ messageHandler(){ console.log('当message改变后,执行这里') } } }
以上即是 vue-property-decorator 增长装饰器的实现方式,对其余装饰器感兴趣,能够看仓库源码,作进一步了解,思路都大同小异。
以上若有误差欢迎指正学习,谢谢。~~~~
github博客地址:https://github.com/WYseven/blog,欢迎star。
若是对你有帮助,请关注【前端技能解锁】: