Typescript 夜點心:修飾器

今天的夜點心咱們來聊聊 Typescript 中的修飾器git

開發中咱們會遇到一類邏輯,他們與特定的類沒有耦合關係,甚至與特定的接口(interface)也沒有耦合關係。咱們能夠把他們抽離出來,並經過某種語法再添回到特定的屬性和方法上去,實現邏輯的解耦和複用,這即是修飾器。github

在基於 Typescript 開發的庫中時常能見到修飾器的身影(也有一部分 JS 庫使用 @babel/plugin-proposal-decorators 來支持修飾器):好比 Angular 中大量利用修飾器的語法來標記組件的生命週期、屬性的性質,Mobx 利用修飾器來爲組件掛載外部狀態等。typescript

要使用修飾器首先須要將 tsconfig.json 中的 compileOptions.experimentalDecorators 字段設爲 true。Typescript 中的修飾器能夠用在如下 5 種場景中:編程

  • 屬性
  • 方法
  • 存取器:即 getter / setter
  • 參數

下面介紹相對最經常使用的屬性和方法修飾器json

屬性修飾器

全部修飾器的本質都是函數,屬性修飾器的函數簽名以下:babel

type PropertyDecorator = (
  target: Object, // 被修飾的類實例或對象
  propertName: string | symbol, // 被修飾的屬性,方法,存取器的名稱
) => void;
複製代碼

在屬性修飾器中咱們能夠經過 Object.defineProperty 來修改一個屬性的 descriptor,從而實現一些通用的邏輯。app

例以下面的 state 修飾器經過修改屬性的 getter/setter 實現了根據屬性變動自動執行重渲染的邏輯:框架

// 修飾器定義,範型約束保證了該修飾器只能用於具備 render 方法的類上
function state<T extends { render(): void }>(
  target: T,
  propertyName: string
) {
  let realValue = target[propertyName];
  Object.defineProperty(target, propertyName, {
    set(value: T) {
      if (value !== realValue) {
        realValue = value;
        target.render();
      }
    },
    get() {
      return realValue;
    }
  })
}

// 用法
class Sprite {
  @state value = 1;

  render() {
    console.log('render');
  }
}

const sprite = new Sprite();
sprite.value = 2; // "render"
複製代碼

上面的精靈 sprite 會在 value 屬性被賦值時自動執行 render 方法,實現了簡易的重渲染邏輯。函數

方法修飾器

方法修飾器相對屬性修飾器多了第三個入參 descriptor 和可選返回值,這使得它的簽名與咱們熟悉的 Object.defineProperty 的簽名很是類似:ui

type MethodDecorator = <T>(
  target: Object,
  key: string | symbol,
  descriptor: TypedPropertyDescriptor<T>, // 方法的描述對象
) => TypedPropertyDescriptor<T> | void; // 可選的返回描述對象
複製代碼

經過 descriptor.value 咱們能夠取到修飾前的方法,而後對它進行包裝,添加邏輯。

例以下面的 log 修飾器爲方法添加了日誌功能,讓方法在調用先後都會向控臺輸出入參和結果:

// 修飾器聲明
function log( target: Object, propertyName: string, propertyDesciptor: PropertyDescriptor ) {
  // 獲取被修飾前的方法
  const original = propertyDesciptor.value;

  propertyDesciptor.value = function (...params: any[]) {
    // 執行前日誌
    console.log(`${propertyName} params`, params);
    // 執行真正的方法
    const result = original.apply(this, params);
    // 執行後日志
    console.log(`${propertyName} result`, result);
    // 返回結果
    return result;
  }

  // 返回描述對象
  return propertyDesciptor;
};

// 用法
class Sprite {
  value = 1

  @log
  render() {
    // 重渲染邏輯
  }
}
複製代碼

工廠 vs 單例

上面咱們的修飾器都是寫做「單例」形式的,也就是對全部被修飾的方法和屬性,咱們都用同一個修飾器來實現修飾。

實際生產中建議將修飾器設計成工廠函數的形式以便擴展更多的功能,像下面的 format 修飾器能根據傳入的模板來修飾對象屬性:

const format = (template: string) => (target: any, name: string) => {
  let real = target[name];
  Object.defineProperty(target, name, {
    set(value) {
      real = value;
    },
    get() {
      return template.replace('$', real);
    }
  })
}

class Foo {
  @format('text的值爲$') text = '';
}
複製代碼

以上就是修飾器相關的內容。若是你在項目中尚未使用過修飾器,能夠嘗試一下,也許會取得不錯的效果。

不過千萬不要刻意爲了使用修飾器而使用它們,不一樣的編程範式和框架會經過不一樣的方式來解決相似的問題。修飾器的思想源於面向對象編程的修飾模式,類似的邏輯複用在 React 中能夠經過 Hooks 來實現。就像在實際開發中你不會常常寫自定義 Hooks,修飾器解決的邏輯問題通常來講對庫開發更爲常見。

github 原文連接

擴展閱讀

相關文章
相關標籤/搜索