在Mobx中是使用装饰器设计模式来实现观察值的,所以为了进一步了解Mobx须要对装饰器模式有必定的认识。此文从设计模式触发到ES7中装饰器语法的应用再到经过babel观察转换@语法,了解es7装饰器语法的具体实现细节。javascript
首先阐述下什么是装饰者模式html
定义:在不改变原对象的基础上,在程序运行期间动态地给对象添加一些额外职责。试原有对象能够知足用户更复杂的需求。java
特色:node
要正确的理解设计模式,首先要明白它是由于什么问题被提出来的。python
在传统的面向对象语言中,咱们给对象添加职责一般是经过继承来实现,而继承有不少缺点:es6
举个例子:一个咖啡店有四种类型的咖啡豆,假如咱们为四种不一样类型的咖啡豆定义了四个类,可是咱们还须要给它们添加不一样的口味(一共有五种口味),所以若是经过继承来将不一样种类的咖啡展现出来须要建立4x5(20)个类(还不包括混合口味),而经过装饰者模式只须要定义五种不一样的口味类将它们动态添加到咖啡豆类便可实现。数据库
经过上述例子,咱们能够发现经过装饰者模式能够实现动态灵活地向对象添加职责而没有显式地修改原有代码,大大减小了须要建立的类数量,它弥补了继承的不足,解决了不一样类之间共享方法的问题npm
它的具体使用场景以下:编程
以游戏中的角色为例,众所周知,游戏中的角色都有初始属性(hp、def、attack),而咱们经过给角色装配装备来加强角色的属性值。设计模式
var role = {
showAttribute: function() {
console.log(`初始属性:hp: 100 def: 100 attack: 100`)
}
}
复制代码
这时咱们经过给角色穿戴装饰装备提升他的属性,咱们能够这样作。
var showAttribute = role.showAttribute
var wearArmor = function() {
console.log(`装备后盔甲属性:hp: 200 def: 200 attack: 100`)
}
role.showAttribute = function() {
showAttribute()
wearArmor()
}
var showAttributeUpgrade = role.showAttribute
var wearWepeon = function() {
console.log(`装备武器后属性:hp: 200 def: 200 attack: 200`)
}
role.showAttribute = function() {
showAttributeUpgrade()
wearWepeon()
}
复制代码
经过这样将一个对象放入另外一个对象,动态地给对象添加职责而没有改变对象自身,其中wearArmor和wearWepeon为装饰函数,它们装饰了role对象的showAttribute这个方法造成了一条装饰链,当函数执行到此时,会自动将请求转发至下一个对象。
除此以外,咱们还能够观察出,在装饰者模式中,咱们不能够在不了解showAttribute这个原有方法的具体实现细节就能够对其进行扩展,而且原有对象的方法照样能够原封不动地进行调用。
在JS中,咱们能够很容易地给对象扩展属性和方法,但若是咱们想给函数添加额外功能的话,就不可避免地须要更改函数的源码,好比说:
function test() {
console.log('Hello foo')
}
复制代码
function test() {
console.log('Hello foo')
console.log('Hello bar')
}
复制代码
这种方式违背了面向对象设计原则中的开放封闭原则,经过侵犯模块的源代码以实现功能的拓展是一个糟糕的作法。
针对上述问题,一种常见的解决方法是设置一个中间变量缓存函数引用,能够对上述函数作以下改动:
var test = function() {
console.log('Hello foo')
}
var _test = test
test = function() {
_test()
console.log('Hello bar')
}
复制代码
经过缓存函数引用实现了函数的拓展,可是这种方式仍是存在问题:除了会在装饰链过长的状况下引入过多中间变量难以维护,还会形成this劫持发生致使不易察觉的bug,虽然this劫持问题能够经过call修正this指向,但仍是过于麻烦。
为了解决上述痛点,咱们能够引入AOP(面向切面编程)这种模式。那么什么是面向切面编程呢,简而言之就是将一些与核心业务逻辑无关的功能抽离出来,以动态的方式加入业务模块,经过这种方式保持核心业务模块的代码纯净和高内聚性以及可复用性。这种模式普遍被应用在日志系统、错误处理。
而实现它们也很是简单,只须要给函数原型扩展两个函数便可:
Function.prototype.before = function(beforeFunc) {
let that = this
return function() {
beforeFunc.apply(this,arguments)
return that.apply(this,arguments)
}
}
Function.prototype.after = function(afterFunc) {
let that = this
return function() {
let ret = that.apply(this,arguments)
afterFunc.apply(this,arguments)
return ret
}
}
复制代码
假设有个需求须要在更新数据库先后都打印相应日志,运用AOP咱们能够这样作:
function updateDb() {
console.log(`update db`)
}
function beforeUpdateDb() {
console.log(`before update db`)
}
function afterUpdateDb() {
console.log(`updated db`)
}
updateDb = updateDb.before(beforeUpdateDb).after(afterUpdateDb)
复制代码
经过这种方式咱们能够灵活地实现了对函数的扩展,避免了函数被和业务无关的代码侵入,增长了代码的耦合度。
装饰者模式自己的设计理念是很是可取的,但仍是能够发现上述代码的实现方式仍是过于臃肿,不如python这类语言从语言层面支持装饰器实现装饰模式来得简洁明了,所幸javascript如今也引入了这个概念,咱们能够经过babel使用它。
ES7中也引入了decorator这个概念,而且经过babel能够获得很好的支持。本质上来讲,decorator和class同样只是一个语法糖而已,可是却很是有用,任何装饰者模式的代码经过decorator均可以以更加清晰明了的方式得以实现。
首先须要安装babel:
npm install babel-loader babal-core babel-preset-es2015 babel-plugin-transform-decorators-legacy
复制代码
在工做区目录下新建.babelrc文件
{
"presets": [
// 把es6转成es5
'es2015'
],
// 处理装饰器语法
"plugins": ['transform-decorators-legacy']
}
复制代码
这样准备工做就完成了,就可使用babel来将带decorator的代码转换成es5代码了
babel index.js > index.es5.js
复制代码
或者咱们也能够经过babel-node index.js
直接执行代码输出结果
decorator使咱们可以在编码时对类、属性进行修改提供了可能,它的原理是利用了ES5当中的
Object.defineProperty(target,key,descriptor)
复制代码
其中最核心的就是descriptor
——属性描述符。
属性描述符分为两种:数据描述符和访问器描述符,描述符必须是二者之一,但不能同时包含二者。咱们能够经过ES5中的Object.getOwnPropertyDescriptor
来获取对象某个具体属性的描述符:
数据描述符:
var user = {name:'Bob'}
Object.getOwnPropertyDescriptor(user,'name')
// 输出
/**
{
"value": "Bob",
"writable": true,
"enumerable": true,
"configurable": true
}
**/
复制代码
访问器描述符:
var user = {
get name() {
return name
},
set name(val) {
name = val
}
}
// 输出
/**
{
"get": f name(),
"set": f name(val),
"enumerable": true,
"configurable": true
}
**/
复制代码
来观察一个简单的ES6类:
class Coffee {
toString() {
return `sweetness:${this.sweetness} concentration:${this.concentration}`
}
}
复制代码
执行这段代码,给Coffee.prototype注册一个toString属性,功能与下述代码类似:
Object.defineProperty(Coffee.prototype, 'toString', {
value: [Function],
enumerable: false,
configurable: true,
writable: true
})
复制代码
当咱们经过装饰器给Coffee类标注一个属性让其变成一个只读属性时,能够这样作:
class Coffee {
@readonly
toString() {
return `sweetness:${this.sweetness} concentration:${this.concentration}`
}
}
复制代码
这段代码等价于:
let descriptor = {
value: [Function],
enumerable: false,
configurable: true,
writable: true
};
descriptor = readonly(Coffee.prototype, 'toString', descriptor) || descriptor;
Object.defineProperty(Coffee.prototype, 'toString', descriptor);
复制代码
从上面代码能够看出,装饰器是在Object.defineProperty
为Coffee.prototype注册toString属性前对其进行拦截,执行一个函数名为readonly的装饰函数,这个装饰函数接收是三个参数,它的函数签名和Object.defineProperty
一致,分别表示:
这个函数的做用就是将descroptor这个参数的数据描述属性writable由true改成false,从而使得目标对象的属性不可被更改。
假设咱们须要给咖啡类增长一个增长甜度和增长浓度的方法,能够这样实现:
function addSweetness(target, key, descriptor) {
const method = descriptor.value
descriptor.value = (...args) => {
args[0] += 10
const ret = method.apply(target, args);
return ret
}
return descriptor
}
function addConcentration(target, key, descriptor) {
const method = descriptor.value
descriptor.value = (...args) => {
args[1] += 10
const ret = method.apply(target, args)
return ret
}
return descriptor
}
class Coffee {
constructor(sweetness = 0, concentration=10) {
this.init(sweetness, concentration)
}
@addSweetness
@addConcentration
init(sweetness, concentration) {
this.sweetness = sweetness // 甜度
this.concentration = concentration; // 浓度
}
toString() {
return `sweetness:${this.sweetness} concentration:${this.concentration}`
}
}
const coff = new Coffee()
console.log(`${coff}`)
复制代码
首先看看输出结果sweetness:10 concentration:20
,能够看出经过addSweetness
和addConcentration
这两个装饰器方法装饰在init方法,经过descriptor.value
得到init方法并用中间变量缓存,而后从新给descriptor.value赋值一个代理函数,在代理函数内部经过arguments
接收init
方法传来的实参并进行改动后从新执行以前的缓存函数获得计算结果。至此咱们便经过decorator的形式成功实现了需求。
从这里咱们能够看出装饰器模式的优点了,能够对某个方法进行叠加使用,而不对原有代码有过强的侵入性,方便复用又能够快速增删。
当须要给咖啡类加冰块时,至关于赋予了它一个新的属性,这时能够经过将decorator做用在类上面,对类进行加强。
function addIce(target) {
target.prototype.iced = true
}
@addIce
class Coffee {
constructor(sweetness = 0, concentration = 10) {
this.init(sweetness, concentration);
}
init(sweetness, concentration) {
this.sweetness = sweetness; // 甜度
this.concentration = concentration; // 浓度
}
toString() {
return `sweetness:${this.sweetness} concentration:${this.concentration} iced:${this.iced}`;
}
}
const coff = new Coffee()
console.log(`${coff}`)
复制代码
先看看输出结果sweetness:0 concentration:10 iced:true
,经过做用在类上的装饰器成功给类的原型添加了属性。 当decorator做用在类上时,只会传入一个参数,就是类自己,在装饰方法中经过变动类的原型给其增长属性。
当想要经过一个decorator做用在不一样的目标上有不一样的表现时,咱们能够将decorator用工厂模式实现:
function decorateTaste(taste) {
return function(target) {
target.taste = taste;
}
}
@decorateTaste('bitter')
class Coffee {
toString() {
return `taste:${Coffee.taste}`;
}
}
@decorateTaste('sweet')
class Milk {
toString() {
return `taste:${Milk.taste}`;
}
}
复制代码
decorator虽然只是语法糖,但却有很是多的应用场景,这里简单提一个AOP的应用场景,也和前面提到的ES5实现的版本有一个对比。
function AOP(beforeFn, afterFn) {
return function(target, key, descriptor) {
const method = descriptor.value
descriptor.value = (...args) => {
let ret
beforeFn && beforeFn(...args)
ret = method.apply(target, args)
if (afterFn) {
ret = afterFn(ret)
}
return ret
}
}
}
// 给sum函数每一个参数进行+1操做
function before(...args) {
return args.map(item => item + 1)
}
// 接收sum函数求的和再执行后置操做
function after(sum) {
return sum + 66
}
class Calculate {
@AOP(before, after)
static sum(...args) {
return args.reduce((a, b) => a + b)
}
}
console.log(Calculate.sum(1, 2, 3, 4, 5, 6))
复制代码
经过将AOP的装饰器函数做用在类方法上能够实现对函数的参数进行前置处理,再对目标函数输出结果进行 后置处理。与ES5实现相比,避免了污染函数原型,经过一种清晰灵活的方式实现,减小了代码量。
在了解装饰器模式和decorator
的基本知识后,终于进入正题了,babel内部是如何装饰器@语法呢。
简单看官网上的一个示例:
import { observable, computed, action } from "mobx";
class OrderLine {
@observable price = 0;
@observable amount = 1;
@computed get total() {
return this.price * this.amount;
}
@action.bound
increment() {
this.amount++
}
}
复制代码
经过babel装饰器插件将其转换为ES5代码,观察@语法被转换的结果,分析下转换以后的代码逻辑。(转换这段代码须要安装babel-preset-mobx
这个预设)
首先来看针对price
属性的装饰器语法:
@observable price = 0;
复制代码
这段代码主要作的事情就是声明一个属性成员price,而后将装饰器函数应用至该属性从而起到了装饰的做用,具体伪代码以下:
// _initializerDefineProperty方法的做用就是经过Object.defineProperty为orderLine这个类定义属性成员,
// 而其中的_descriptor为通过装饰后的属性描述符,该值由_applyDecoratedDescriptor方法根据入参返回
// 通过特定装饰器装饰的修饰符
_initializerDefineProperty(this, "price", _descriptor, this);
_descriptor = _applyDecoratedDescriptor(_class.prototype, "price", [observable], {
configurable: true,
enumerable: true,
writable: true,
initializer: function () {
return 0;
}
})
复制代码
能够看出babel转换@语法的关键是经过_applyDecoratedDescriptor
方法,接下来重点解析下此方法。
该函数签名为:
function _applyDecoratedDescriptor(target, property, decorators, descriptor, context)
复制代码
该函数形参各自的含义以下所示:
[observable]
,经过@computed修饰符修饰的装饰器就是[computed]
initializer
这个属性定义初始值,而方法成员没有这个属性,所以可经过此属性区分属性成员和方法成员,在函数内部逻辑有所体现解释完函数签名后,开始进入函数逻辑。
首先要明确这个函数的做用就是根据传入参数返回装饰后的属性描述符,其中最核心的逻辑就是将装饰器循环应用至原有属性,代码以下:
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
复制代码
假设咱们传入的decorators
是[a, b, c],那么上面代码就至关于应用公式a(b(c(property)))
,即装饰器c、b、a前后做用与目标属性,而decorator的函数签名与Object.defineProperty
一致,它的做用就是修改目标属性的描述符。
至此babel转换成@语法的精髓已解释完,它的核心就是_applyDecoratedDescriptor
这个方法,而这个方法主要作的就是将装饰器循环应用至目标属性。
小结一下,@语法的原理就是:
Object.defineProperty
将修改后的属性描述符运用至目标属性、