在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
將修改後的屬性描述符運用至目標屬性、