經過vue-property-decorator源碼學習裝飾器

設計模式中有一個結構型設計模式:裝飾器模式,定義是在不改變原有邏輯的狀況下對其進行包裝拓展從而知足更復雜的需求。在Java中有@annotation用來爲類或方法添加註解,例如@override用於檢查子類是否正確重寫父類方法。ES7和TypeScript一樣也引入了裝飾器,一種特殊類型的聲明,它可以被附加到類聲明,方法, 訪問符,屬性或參數上。javascript

前言

在使用TS和Vue開發的過程當中咱們常用vue-property-decorator這個庫,它封裝了@Component、@Prop、@Watch、@Emit等經常使用裝飾器,用於像原生ES class那樣聲明基於類的Vue組件,接來下咱們就經過vue-property-decorator的源碼來學習裝飾器。前端

  • 裝飾器語法至今尚未離開stage2,在TS中也是一個實驗性的特性。

@Component

@Component裝飾器實際上是vue-class-component庫提供的,首先來看它的定義,@Component是一個類裝飾器,類裝飾器接收類的構造函數做爲入參。vue

// index.ts
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  if (typeof options === 'function') {
    return componentFactory(options)
  }
  return function (Component: VueClass<Vue>) {
    return componentFactory(Component, options)
  }
}

複製代碼

@Component有一個入參options,是爲了方便用戶對組件類進行一些額外屬性的聲明,內部判斷options是不是函數以區分不一樣的調用方式:java

// 默認傳入類構造函數
@Component
export default class HelloWorld extends Vue {}

// 傳入options對象
@Component({name: 'HelloWorld'})
export default class HelloWorld extends Vue {}
複製代碼

接下來調用了工廠函數componentFactory,實際上componentFactory幹了幾件事:git

  • 將類原型上的屬性按照不一樣類型(data、methods、mixins、computed)添加到options中
  • 將mixins中的data屬性依賴收集
  • 返回一個傳入options的新構造器
function componentFactory(Component, options = {}) {
    options.name = options.name || Component._componentTag || Component.name;
    // prototype props.
    const proto = Component.prototype;
    // 按類型添加到options
    Object.getOwnPropertyNames(proto).forEach(function (key) {
        if (key === 'constructor') {
            return;
        }
        // 判斷傳入屬性是否在白名單
        if ($internalHooks.indexOf(key) > -1) {
            options[key] = proto[key];
            return;
        }
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor.value !== void 0) {
            // methods
            if (typeof descriptor.value === 'function') {
                (options.methods || (options.methods = {}))[key] = descriptor.value;
            }
            else {
                // typescript decorated data
                (options.mixins || (options.mixins = [])).push({
                    data() {
                        return { [key]: descriptor.value };
                    }
                });
            }
        }
        else if (descriptor.get || descriptor.set) {
            // computed properties
            (options.computed || (options.computed = {}))[key] = {
                get: descriptor.get,
                set: descriptor.set
            };
        }
    });
    // 依賴收集
    (options.mixins || (options.mixins = [])).push({
        data() {
            return collectDataFromConstructor(this, Component);
        }
    });
    // 將類中添加裝飾器的方法取出來執行後刪除__decorators__,填充__decorators__的邏輯下方介紹@Prop時有說起
    const decorators = Component.__decorators__;
    if (decorators) {
        decorators.forEach(fn => fn(options));
        delete Component.__decorators__;
    }
    // 找到父類並建立一個新的構造器
    const superProto = Object.getPrototypeOf(Component.prototype);
    const Super = superProto instanceof Vue
        ? superProto.constructor
        : Vue;
    const Extended = Super.extend(options);
    // 檢測options中key值合法性
    forwardStaticMembers(Extended, Component, Super);
    // 元編程相關
    if (reflectionIsSupported()) {
        copyReflectionMetadata(Extended, Component);
    }
    return Extended;
}
複製代碼

從@Component中咱們能夠看到類裝飾器能夠經過對構造器的修改來爲原有類作無感知的修改,裝飾器是在編譯時做用的,因此沒法影響類的實例,可是能夠經過修改類的原型來影響實例。github

@Prop

咱們經過@Prop來學習屬性裝飾器,@Prop是一個屬性裝飾器,實現爲一個裝飾器工廠,return的函數參數有兩個,一個是類的構造函數或者原型對象,一個是裝飾的成員名稱。typescript

function Prop(options) {
    // 保證options是一個對象
    if (options === void 0) { options = {}; }
    // 返回一個工廠函數,target對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。key是成員的名字
    return function (target, key) {
        // 獲取元數據
        applyMetadata(options, target, key);
        // 高階函數,做用是設置組件的props
        createDecorator(function (componentOptions, k) {
            ;
            (componentOptions.props || (componentOptions.props = {}))[k] = options;
        })(target, key);
    };
}
// 判斷是否支持反射API
var reflectMetadataIsSupported = typeof Reflect !== 'undefined' && typeof Reflect.getMetadata !== 'undefined';
function applyMetadata(options, target, key) {
    // 若是支持反射API,取出相應元數據
    if (reflectMetadataIsSupported) {
        if (!Array.isArray(options) &&
            typeof options !== 'function' &&
            typeof options.type === 'undefined') {
            options.type = Reflect.getMetadata('design:type', target, key);
        }
    }
}
// 高階函數,將裝飾器都push到@Component的__decorators__中,@Prop是將設置props的方法push進去
function createDecorator(factory) {
    return (target, key, index) => {
        const Ctor = typeof target === 'function'
            ? target
            : target.constructor;
        if (!Ctor.__decorators__) {
            Ctor.__decorators__ = [];
        }
        if (typeof index !== 'number') {
            index = undefined;
        }
        Ctor.__decorators__.push(options => factory(options, key, index));
    };
}

複製代碼

我看到這個createDecorator方法時震驚於包做者的機智,包中幾乎全部的屬性裝飾器都是基於這個函數實現的,包括Inject、InjectReactive、Provide、ProvideReactive、Model、Prop、PropSync、Watch、Ref,這個高階函數能將對屬性裝飾器的操做都Push到一個數組中,再由@Component取出執行並統一修改裝飾的類實例,而且因爲閉包的關係塞進@Component數組中的方法都擁有外部屬性的訪問權限,能夠將不一樣的屬性裝飾器寫在各自的方法中。(換我來寫這一段大概就是push一個{type: 'Prop'}進去了,而後執行的時候作if判斷,在大佬這裏學到了策略模式的更高階玩法,有用的知識增長了!)編程

@Emit

咱們再經過@Emit來學習一下方法裝飾器,方法裝飾器做用在方法的屬性描述符上,能夠用來監視,修改或者替換方法定義,接收三個參數,前兩個參數和@Prop同樣,分別是構造函數(static方法)或原型對象(實例方法)、方法的名稱,多了一個參數是參數的屬性描述符(descriptor),這個屬性描述符很關鍵,其中的value屬性就是被裝飾的方法最後真正要執行的方法。設計模式

// 用於將駝峯命名的方法名轉變爲減號鏈接
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = function (str) { return str.replace(hyphenateRE, '-$1').toLowerCase(); };

function Emit(event) {
    return function (_target, key, descriptor) {
        key = hyphenate(key);
        // 保存原有函數
        var original = descriptor.value;
        descriptor.value = function emitter() {
            var _this = this;
            var args = [];
            // arguments 是觸發這個emit方法的事件列表
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            // 執行vue的$emit方法,並傳入參數
            var emit = function (returnValue) {
                if (returnValue !== undefined)
                    args.unshift(returnValue);
                _this.$emit.apply(_this, [event || key].concat(args));
            };
            // 執行原方法
            var returnValue = original.apply(this, args);
            // 若是原方法返回了promise,執行.then方法後emit,若是是正常返回值就直接emit
            if (isPromise(returnValue)) {
                returnValue.then(function (returnValue) {
                    emit(returnValue);
                });
            }
            else {
                emit(returnValue);
            }
            // 將原方法的返回值返回
            return returnValue;
        };
    };
}
複製代碼

@Emit的核心思路就是截胡函數的執行,在被裝飾的函數被調用後將會進入方法裝飾器,在裝飾器中進行額外的emit操做,使用者就再也不須要手動去調用this.$emit了。數組

方法裝飾器在業務中使用場景比較多,例如咱們能夠將日誌上報或者埋點處理等操做封裝在方法裝飾器中,再用方法裝飾器去修飾相應的方法,這個方法就會獲得功能上的加強,而不用改變方法自己的邏輯,裝飾器和方法的邏輯解耦,大大增長了代碼的可讀性和靈活度,也能夠少些一些重複的代碼了。

訪問器裝飾器 & 參數裝飾器

訪問器裝飾器和參數裝飾器在vue-property-decorator中沒有體現,不過咱們也一樣須要瞭解他們。

早在es5就已經引入了訪問器的概念,能夠爲一個對象設置setter和getter。

class demo1 {
    private x = 1
    @Log()
    get getX() {
        return this.x
    }
}

function Log() {
    return function (target, propertyKey, descriptor) {
        console.log(descriptor)
    }
}
複製代碼

訪問器裝飾器和方法裝飾器用法徹底同樣,這裏就不過多贅述了。

參數裝飾器,用來裝飾函數的參數,接收三個參數(構造函數or原型對象、參數的名字、參數在函數參數列表中的索引),

class demo2 {
    Log(@required msg) {
        console.log(msg)
    }
}

function required(target, propertyKey, parameterIndex) {
    // do something
}
複製代碼

參數裝飾器只能用來監視一個方法的參數是否被傳入,參數裝飾器的返回值會被忽略

總結

在項目中合理的使用裝飾器能提升代碼開發效率,促進代碼解耦,提高代碼的可讀性,你們能夠在TS項目中嘗試使用,裝飾器真香!

不過,裝飾器僅提供類的構造器、屬性、方法、參數的劫持,自己沒有提供附加元數據的功能,若是要使用元數據咱們須要使用反射(Reflect),反射提供在類及其屬性、方法、入參上存儲讀取數據的能力,ES6提供反射API還不可以支持元編程,因此TypeScript在1.5+版本中使用了reflect-metadata庫來提供元編程支持,不過裝飾器元數據在TS中都是實驗性功能中的實驗性功能,未來可能會有破壞性更新,請慎用。有興趣的同窗能夠參考vue-class-component庫中的reflect.ts文件。

我是suhangdev,歡迎與我交流前端相關話題,若是文章對你有幫助,請點贊支持噢,謝謝!

相關文章
相關標籤/搜索