在用typescript时,vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:
- @Emit
- @Inject
- @Model
- @Prop
- @Provide
- @Watch
- @Component (从 vue-class-component 继承)
源码也就200来行,于是就有信心来慢慢读了
为什么需要vue-class-component? 在typescript里写vue 每次都需要写很多额外的形式代码:
而装饰器就是解决这些冗余代码的(实质上并没有减少,只是用一层函数包装了,后面有源码会讲解)
可自行了解一下装饰模式
AOP 面向切面编程
示例:
- 首先创建一个普通的Man类,它的抵御值 2,攻击力为 3,血量为 3;
- 然后我们让其带上钢铁侠的盔甲,这样他的抵御力增加 100,变成 102;
- 让其带上光束手套,攻击力增加 50,变成 53;
- 最后让他增加“飞行”能力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Man { constructor(def = 2, atk = 3, hp = 3) { this.init(def, atk, hp) }
init(def, atk, hp) { this.def = def this.atk = atk this.hp = hp }
toString() { return `防御力: ${this.def},攻击力: ${this.atk},血量:${this.hp}` } }
var tony = new Man() console.log(`当前状态 ===> ${tony}`)
|
然后 创建 decorateArmour 方法,为钢铁侠装配盔甲——注意 decorateArmour 是装饰在方法init上的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| function decorateArmour(target, key, descriptor) { const method = descriptor.value let moreDef = 100 let ret descriptor.value = (...args) => { args[0] += moreDef ret = method.apply(target, args) return ret } return descriptor }
class Man { constructor(def = 2, atk = 3, hp = 3) { this.init(def, atk, hp) }
@decorateArmour init(def, atk, hp) { this.def = def this.atk = atk this.hp = hp }
toString() { return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}` } }
var tony = new Man(); console.log(`当前状态 ===> ${tony}`);
|
Decorators 的本质是利用了 ES5 的 Object.defineProperty 属性,这三个参数其实是和 Object.defineProperty 参数一致的,因此不能更改,详细分析请见 细说 ES7 JavaScript Decorators
同样的代码复制一份,增加攻击力:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function decorateLight(target, key, descriptor) { const method = descriptor.value; let moreAtk = 50; let ret; descriptor.value = (...args)=>{ args[1] += moreAtk; ret = method.apply(target, args); return ret; } return descriptor; }
class Man{ constructor(def = 2,atk = 3,hp = 3){ this.init(def,atk,hp); }
@decorateArmour @decorateLight init(def,atk,hp){ this.def = def; this.atk = atk; this.hp = hp; } ... } var tony = new Man(); console.log(`当前状态 ===> ${tony}`);
|
按装饰模式所言,装饰模式有:纯粹装饰模式和半透明装饰模式
上面两个属于纯粹装饰模式,它不增加对原有类的接口。而下面给普通人增加飞行能力,给类增加新方法,属于半透明的装饰模式,类似适配器模式:
- 增加一个方法
1 2 3 4 5 6 7 8 9 10 11 12
| function addFly(canFly) { return function(target) { target.canFly = canFly let extra = canFly ? '(技能加成:飞行能力)' : '' let method = target.prototype.toString
target.prototype.toString = (...args) => { return method.apply(target.prototype, args) + extra } return target } }
|
- 用这个方法去直接装饰类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function addFly(canFly) { } @addFly(true) class Man { constructor(def = 2, atk = 3, hp = 3) { this.init(def, atk, hp) } @decorateArmour @decorateLight init(def, atk, hp) { this.def = def this.atk = atk this.hp = hp } } console.log(`当前状态 ===> ${tony}`);
|
作用在方法上的decorator接收第一个参数(target)是类的prototype;如果把一个decorator作用到类上,则它的第一个参数target是类本身
经典实现 Logger
有了上面的基础,下面我们来写一个简易版日志系统
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| let log = type => (target, name, descriptor) => { const method = descriptor.value descriptor.value = (...args) => { console.info(`(${type} 正在执行:${name}(${args}) = ?`) let ret try { ret = method.apply(target, args) console.info(`(${tytpe} 成功:${name}(${args}) => ${ret})`) } catch (error) { console.error(`(${tytpe} 失败:${name}(${args}) => ${error})`) } return ret } }
class IronMan { @log('IronMan 自检阶段') check() { return '检查完毕' }
@log('IronMan 攻击阶段') attack() { return '击倒敌人' }
@log('IronMan 机体报错') error() { throw 'something is wrong!' } }
var tony = new IronMan() tony.check(); tony.attack(); tony.error();
|
Logger方法的关键在于:
- 首先使用
const method = descriptor.value
将原有的方法提取出来,保障原有方法的纯净
- 在
try catch
语句是调用 ret = method.apply(target, args)
在调用之前之后分别进行日志汇报
- 最后返回
return ret
原始的调用结果
vue-property-decorator
vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:
- @Emit
- @Inject
- @Model
- @Prop
- @Provide
- @Watch
- @Component (从 vue-class-component 继承)
我们来读读源码上是怎样来实现的吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
| import Vue, {PropOptions, WatchOptions} from 'vue' import Component, {createDecorator} from 'vue-class-component' import 'reflect-metadata'
export type Constructor = { new(...args: any[]): any }
export { Component, Vue }
export function Inject(key?: string | symbol): PropertyDecorator { return createDecorator((componentOptions, k) => { if (typeof componentOptions.inject === 'undefined') { componentOptions.inject = {} } if (!Array.isArray(componentOptions.inject)) { componentOptions.inject[k] = key || k } }) }
export function Provide(key?: string | symbol): PropertyDecorator { return createDecorator((componentOptions, k) => { let provide: any = componentOptions.provide if (typeof provide !== 'function' || !provide.managed) { const original = componentOptions.provide provide = componentOptions.provide = function(this: any) { let rv = Object.create((typeof original === 'function' ? original.call(this) : original) || null) for (let i in provide.managed) rv[provide.managed[i]] = this[i] return rv } provide.managed = {} } provide.managed[k] = key || k }) }
export function Model(event?: string, options: (PropOptions | Constructor[] | Constructor) = {}): PropertyDecorator { return function (target: Vue, key: string) { if (!Array.isArray(options) && typeof (options as PropOptions).type === 'undefined') { (options as PropOptions).type = Reflect.getMetadata('design:type', target, key) } createDecorator((componentOptions, k) => { (componentOptions.props || (componentOptions.props = {}) as any)[k] = options componentOptions.model = { prop: k, event: event || k } })(target, key) } }
export function Prop(options: (PropOptions | Constructor[] | Constructor) = {}): PropertyDecorator { return function (target: Vue, key: string) { if (!Array.isArray(options) && typeof (options as PropOptions).type === 'undefined') { (options as PropOptions).type = Reflect.getMetadata('design:type', target, key) } createDecorator((componentOptions, k) => { (componentOptions.props || (componentOptions.props = {}) as any)[k] = options })(target, key) } }
export function Watch(path: string, options: WatchOptions = {}): MethodDecorator { const { deep = false, immediate = false } = options
return createDecorator((componentOptions, handler) => { if (typeof componentOptions.watch !== 'object') { componentOptions.watch = Object.create(null) } (componentOptions.watch as any)[path] = { handler, deep, immediate } }) }
const hyphenateRE = /\B([A-Z])/g const hyphenate = (str: string) => str.replace(hyphenateRE, '-$1').toLowerCase()
export function Emit(event?: string): MethodDecorator { return function (target: Vue, key: string, descriptor: any) { key = hyphenate(key) const original = descriptor.value descriptor.value = function emitter(...args: any[]) { if (original.apply(this, args) !== false) this.$emit(event || key, ...args) } } }
export function Off(event?: string, method?: string): MethodDecorator { return function (target: Vue, key: string, descriptor: any) { key = hyphenate(key) const original = descriptor.value descriptor.value = function offer(...args: any[]) { if (original.apply(this, args) !== false) { if (method) { if (typeof this[method] === 'function') { this.$off(event || key, this[method]) } else { throw new TypeError('must be a method name') } } else if (event) { this.$off(event || key) } else { this.$off() } } } } }
export function On(event?: string): MethodDecorator { return createDecorator((componentOptions, k) => { const key = hyphenate(k) if (typeof componentOptions.created !== 'function') { componentOptions.created = function () { } } const original = componentOptions.created componentOptions.created = function () { original() if (typeof componentOptions.methods !== 'undefined') { this.$on(event || key, componentOptions.methods[k]) }
} }) }
export function Once(event?: string): MethodDecorator { return createDecorator((componentOptions, k) => { const key = hyphenate(k) if (typeof componentOptions.created !== 'function') { componentOptions.created = function () { } } const original = componentOptions.created componentOptions.created = function () { original() if (typeof componentOptions.methods !== 'undefined') { this.$once(event || key, componentOptions.methods[k]); } } }) }
export function NextTick(method: string): MethodDecorator { return function (target: Vue, key: string, descriptor: any) { const original = descriptor.value descriptor.value = function emitter(...args: any[]) { if (original.apply(this, args) !== false) if (typeof this[method] === 'function') { this.$nextTick(this[method]) } else { throw new TypeError('must be a method name') } } } }
|