使用過 mobx + mobx-react 的同窗對於 ES 的新特性裝飾器確定不陌生。我在第一次使用裝飾器的時候,我就對它愛不釋手,書寫起來簡單優雅,太適合我這種愛裝 X 且懶的同窗了。今天我就帶着你們深刻淺出這個優雅的語法特性:裝飾器。javascript
普及完一些必要的知識點後,咱們繼續進入到咱們的主題:裝飾器。java
裝飾器的制定過程也不是一路順風的,並且就算是2020年初的如今,這個備受爭議的語法特性官方標準還在討論制定當中,目前仍處於 stage-2: 草稿狀態。react
但目前市面上 Babel、TypeScript 編譯支持的裝飾器語法主要包括兩種方式,一個是 傳統方式(legacy) 和目前標準方式。git
因爲目前標準還不是很成熟,編譯器的支持並不全面,因此市面上大部分的裝飾器庫,大都只是兼容 legacy 方式,如 Mobx,以下爲 Mobx 官網中的一段話:github
Note that the legacy mode is important (as is putting the decorators proposal first). Non-legacy mode is WIP.
下面我就從實際場景出發,來使用裝飾器模式來實現咱們常見的一些業務場景。api
注意:因爲新版標準能夠說是在 legacy 的方式下改造出來的,legacy 更加靈活,標準方式則主張靜態配置去擴展實現裝飾器功能babel
我但願實現一個 validate 修飾器,用於定義成員變量的校驗規則,使用以下ide
import {validate, check} from 'validate' class Person { @validate(val => !['M', 'W'].includes(val) && '須要爲 M 或者 W') gender = 'M' } const person = new Person(); person.gender = null; check(person); // => [{ name: 'gender', error: '須要爲 M 或者 W' }]
以上這種方式,相比於運行時 validate,以下prototype
const check = (person) => { const errors = []; if (!['M', 'W'].includes(person.gender)) { errors.push({name: 'gender', error: '須要爲 M 或者 W'}); } return errors; }
裝飾器的方式可以更快捷的維護校驗邏輯,更加具備表驅動程序的優點,只須要改配置便可。可是對於沒有接觸過裝飾器模式模式的同窗,深刻改造裝飾器內部的邏輯就有必定門坎了(可是不怕,這篇文章幫助你們下降門坎)。code
因爲目前 Babel 編譯對於新版標準支持不是很徹底,對於標準的裝飾器模式實現有必定程度的影響,因此本文主要介紹 legacy 方式的實現,相信對於你們後續實現標準的裝飾器也是有幫助的!
按照 api 的使用用例,咱們能夠知道,對於 person 實例是已經注入了 validate 校驗邏輯的,而後在 check
方法中提取校驗邏輯並執行便可。
@validate // 注入校驗邏輯 | check // 提取校驗邏輯並執行 | 返回校驗結果
首先咱們在 babel 配置中須要以下配置:
"plugins": [ [ "@babel/proposal-decorators", { "legacy": true } ], ["@babel/proposal-class-properties", { "loose": true }] ]
對於咱們須要實現的 @validate
裝飾器結構以下:
// rule 爲外界自定義校驗邏輯 function validate(rule) { // target 爲原型,也就是 Person.prototype // keyName 爲修飾的成員名,如 `gender` // descriptor 爲該成員的是修飾實體 return (target, keyName, descriptor) => { // 注入 rule target['check'] = target['check'] || {}; target['check'][keyName] = rule; return descriptor; } }
根據上述邏輯,執行完 @validate
以後,在 Person.prototype
中會注入 'check'
屬性,同時咱們在 check
方法中拿到該屬性便可進行校驗。
那麼咱們是否是完成了該方法呢?其實還遠遠不夠:
check
屬性須要足夠隱藏,同時屬性名 check
未免太容易被實例屬性覆蓋,從而不能經過原型鏈找到該屬性check
屬性可能會丟失,甚至會污染校驗規則首先咱們來看第一個問題:改造咱們的代碼
const getInjectPropName = typeof Symbol === 'function' ? name => Symbol.for(`[[${name}]]`) : name => `[[${name}]]` const addHideProps = (target, name, value) => { Object.defineProperty(target, name, { enumerable: false, configurable: true, writable: true, value }) } function validate(rule) { return (target, keyName, descriptor) => { const name = getInjectPropName('check'); addHideProps(target, name, target[name] || {}); target[name][keyName] = rule; return descriptor; } }
相比於以前的代碼實現,這樣 Object.keys(Person.prototype)
不會包含 check
屬性,同時也大大下降了屬性命名衝突的問題。
對於第二個問題,類繼承模式下的裝飾器書寫。以下例子:
class Person { @validate(val => !['M', 'W'].includes(val) && '須要爲 M 或者 W') gender = 'M' @validate(a => !(a > 10) && '須要大於10') age = 12 } class Man extends Person { @validate(val => !['M'].includes(val) && '須要爲 M') gender = 'M' }
其中的原型鏈模型圖以下
person instance +-------------------+ +----------+ | Person.prototype | |__proto___+------>------------------+ | |+ | rules | +----------+ +-------+--+-+------+ | | ^ ^ ^ | | | | | | | | | +----------+ | | | rules +- -- -- -- -- | | +----------+ | | | | | person instance+ +----------+ | |__proto___| | man instance | |+ +-----------+ +----------+ | |__proto__ | | | | | +---->+ | +-----------+ | | | | | +----------+ | | | rules | | | | +---^------+ | | | | | | +-----------+ | rules | - - - - - -- - - -+ +-----------+
能夠看到 man instance 和 person instance 共享同一份 rules,同時 Man
中的 validate
已經污染了共享的這份 rules,致使 person instance
校驗邏輯
因此咱們須要把原型模型修改成以下模式:
person instance +-------------------+ +----------+ | Person.prototype | |__proto___+------>------------------+ | |+ | rules | +----------+ +-------+-----------+ | | ^ | | | | | +----------+ | | rules +- -- -- -- -- +----------+ person instance2 Man.prototype +----------+ |__proto___| man instance | | +-----------+ +----------+ |__proto__ | | | | +---->+ | +-----------+ | | | | +----------+ | | | rules | | | +---+------+ | | ^ | | | +-----------+ | | rules | - - - - + +-----------+
能夠看到 man instance
和 person instance
都有一份 rules
在其原型鏈上,這樣就不會有污染的問題,同時也不會丟失校驗規則
修改咱們的代碼:
const getInjectPropName = typeof Symbol === 'function' ? name => Symbol.for(`[[${name}]]`) : name => `[[${name}]]` const addHideProps = (target, name, value) => { Object.defineProperty(target, name, { enumerable: false, configurable: true, writable: true, value }) } function validate(rule) { return (target, keyName, descriptor) => { const name = getInjectPropName('check'); // 沒有注入過 rules if (!target[name]) { addHideProps(target, name, {}); } else { // 已經注入,可是是注入在 target.__proto__ 中 // 也就是繼承模式 if (!target.hasOwnProperty(name)) { // 淺拷貝一份至 own addHideProps(target, name, {...target[name]}) } } target[name][keyName] = rule; return descriptor; } }
如上,纔算是咱們完備的代碼!並且 mobx 也是有相同場景的考慮的。
總結是把以上模式沉澱爲 decorate-utils 方便咱們自定義本身的修飾器