TypeScript 裝飾器的執行原理


裝飾器本質上提供了對被裝飾對象 Property​ Descriptor 的操作,在運行時被調用。

因為對於同一對象來說,可同時運用多個裝飾器,然后裝飾器中又可對被裝飾對象進行任意的修改甚至是替換掉實現,直觀感覺會有一些主觀認知上的錯覺,需要通過代碼來驗證一下。

比如,假若每個裝飾器都對被裝飾對象的有替換,其結果會怎樣?

多個裝飾器的應用

通過編譯運行以下示例代碼並查看其結果可以得到一些直觀感受:

function f() {
  console.log("f(): evaluated");
  return function(_target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
      console.log(`[f]before ${key} called`, args);
      const result = original.apply(this, args);
      console.log(`[f]after ${key} called`);
      return result;
    };
    console.log("f(): called");
    return descriptor;
  };
}

function g() {
console.log("g(): evaluated");
return function(_target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(</span>[g]before ${<span class="pl-smi">key</span>} called<span class="pl-pds">, args);
const result = original.apply(this, args);
console.log(</span>[g]after ${<span class="pl-smi">key</span>} called<span class="pl-pds">);
return result;
};
console.log("g(): called");
return descriptor;
};
}

class C {
@f()
@g()
foo(count: number) {
console.log(</span>foo called ${<span class="pl-smi">count</span>}<span class="pl-pds">);
}
}

const c = new C();
c.foo(0);
c.foo(1);

先放出執行結果:

f(): evaluated
g(): evaluated
g(): called
f(): called
[f]before foo called [ 0 ]
[g]before foo called [ 0 ]
foo called 0
[g]after foo called [ 0 ]
[f]after foo called [ 0 ]
[f]before foo called [ 1 ]
[g]before foo called [ 1 ]
foo called 1
[g]after foo called [ 1 ]
[f]after foo called [ 1 ]

下面來詳細分析。

編譯后的裝飾器代碼

首頁看看編譯后變成 JavaScript 的代碼,畢竟這是實際運行的代碼:

編譯后的代碼
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
function f() {
    console.log("f(): evaluated");
    return function (_target, key, descriptor) {
        var original = descriptor.value;
        descriptor.value = function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            console.log("[f]before " + key + " called", args);
            var result = original.apply(this, args);
            console.log("[f]after " + key + " called", args);
            return result;
        };
        console.log("f(): called");
        return descriptor;
    };
}
function g() {
    console.log("g(): evaluated");
    return function (_target, key, descriptor) {
        var original = descriptor.value;
        descriptor.value = function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            console.log("[g]before " + key + " called", args);
            var result = original.apply(this, args);
            console.log("[g]after " + key + " called", args);
            return result;
        };
        console.log("g(): called");
        return descriptor;
    };
}
var C = /** @class */ (function () {
    function C() {
    }
    C.prototype.foo = function (count) {
        console.log("foo called " + count);
    };
    __decorate([
        f(),
        g(),
        __metadata("design:type", Function),
        __metadata("design:paramtypes", [Number]),
        __metadata("design:returntype", void 0)
    ], C.prototype, "foo", null);
    return C;
}());
var c = new C();
c.foo(0);
c.foo(1);

先看經過 TypeScript 編譯后的代碼,重點看這一部分:

var C = /** @class */ (function () {
    function C() {
    }
    C.prototype.foo = function (count) {
        console.log("foo called " + count);
    };
    __decorate([
        f(),
        g(),
        __metadata("design:type", Function),
        __metadata("design:paramtypes", [Number]),
        __metadata("design:returntype", void 0)
    ], C.prototype, "foo", null);
    return C;
}());

tslib 中裝飾器的實現

其中 __decorate 為 TypeScript 經 tslib 提供的 Decorator 實現,其源碼為:

tslib/tslib.js(經過格式化)

var __decorate =
  (this && this.__decorate) ||
  function(decorators, target, key, desc) {
    var c = arguments.length,
      r =
        c < 3
          ? target
          : desc === null
          ? (desc = Object.getOwnPropertyDescriptor(target, key))
          : desc,
      d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
      r = Reflect.decorate(decorators, target, key, desc);
    else
      for (var i = decorators.length - 1; i >= 0; i--)
        if ((d = decorators[i]))
          r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
  };

裝飾器的執行順序

配合編譯后代碼和這里裝飾器的實現來看,進一步之前了解到的關於裝飾器被求值和執行的順序,

源碼中應用裝飾器的地方:

  @f()
  @g()
  foo(count: number) {
    console.log(`foo called ${count}`);
  }

然后這里的 @f() @g() 按照該順序傳遞給了 __decorate 函數,

  __decorate(
    [
+      f(),
+      g(),
      __metadata("design:type", Function),
      __metadata("design:paramtypes", [Number]),
      __metadata("design:returntype", void 0)
    ],
    C.prototype,
    "foo",
    null
  );

然后在 __decorate 函數體中,對傳入的 decorators 從數據最后開始,取出裝飾器函數順次執行,

var __decorate =
  (this && this.__decorate) ||
  function(decorators, target, key, desc) {
    var c = arguments.length,
      r =
        c < 3
          ? target
          : desc === null
          ? (desc = Object.getOwnPropertyDescriptor(target, key))
          : desc,
      d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
      r = Reflect.decorate(decorators, target, key, desc);
    else
+      for (var i = decorators.length - 1; i >= 0; i--)
        if ((d = decorators[i]))
          r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
  };

其中 r 便是裝成器的返回,會被當作被裝飾對象的新的屬性描述器(Property Descriptor)來重新定義被裝飾的對象:

Object.defineProperty(target, key, r)

所以,像示例代碼中多個裝飾器均對被裝飾對象有修改,原則上和多次調用 Object.defineProperty() 相當。

Object.defineProperty()

而調用 Object.defineProperty() 的結果是后面的會覆蓋前面的,比如來看這里一個簡單的示例:

const obj = {};

Object.defineProperty(obj, "foo", {
configurable: true,
value: function() {
console.log("1");
}
});

Object.defineProperty(obj, "foo", {
value: function() {
console.log("2");
}
});

obj.foo(); // 2

注意: 根據 MDN 對 defineProperty 的描述configurable 在缺省時為 false,所以如果要重復定義同一個 key,需要顯式將其置為 true

configurable

true if and only if the type of this property descriptor may be changed and if the > property may be deleted from the corresponding object.
Defaults to false.

回到本文開頭的示例,為了進一步驗證,可通過將運用裝飾之后的屬性描述器打印出來:

console.log(Object.getOwnPropertyDescriptor(C.prototype, "foo").value.toString());

輸出結果為:

function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            console.log("[f]before " + key + " called", args);
            var result = original.apply(this, args);
            console.log("[f]after " + key + " called", args);
            return result;
        }

那么這里引出另一個問題,通過裝飾器重復定義同一屬性時,並沒有顯式返回一個 configurable:true 的對象,那為何在運用多個裝飾器重復定義時沒報錯。

裝飾器入參中的 descriptor

答案就只有一個,那就是裝飾器傳入的 descriptor 已經是 configurabletrue 的狀態。

為了驗證,只需要在 @f()@g() 任意一個裝飾器中將 descriptor 打印出來即可。

function g() {
  console.log("g(): evaluated");
  return function(_target: any, key: string, descriptor: PropertyDescriptor) {
+      console.log(descriptor)
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
      console.log(`[g]before ${key} called`, args);
      const result = original.apply(this, args);
      console.log(`[g]after ${key} called`, args);
      return result;
    };
    console.log("g(): called");
    return descriptor;
  };
}

輸出的 descriptor

{
  value: [Function],
  writable: true,
  enumerable: true,
  configurable: true
}

這便是最終運行時會執行的 foo 方法真身。

可以看到確實是最后生效的裝飾器確實是后運用的 @f()。因此你確實可以這么理解多個裝飾器的重疊應用為,那一切都還說得通,就是 后運用的裝飾器中 對被裝飾對象的替換 會覆蓋掉 先運用的裝飾器 對被裝飾對象的替換。

But,

這解釋不了它的輸出結果:

f(): evaluated
g(): evaluated
g(): called
f(): called
[f]before foo called [ 0 ]
[g]before foo called [ 0 ]
foo called 0
[g]after foo called
[f]after foo called
[f]before foo called [ 1 ]
[g]before foo called [ 1 ]
foo called 1
[g]after foo called
[f]after foo called

裝飾器嵌套

原因就在於這句代碼:

var result = original.apply(this, args);

因為這句,@f()@g() 便不是簡單的覆蓋關系,而是形成了嵌套關系。

這里 originaldescriptor.value,即裝飾器傳入的 descriptor 的一個副本。我們在進行覆蓋前保存了一下原方法的副本,

// 保存原始的被裝飾對象
const original = descriptor.value;

// 替換被裝飾對象
descriptor.value = function(...args: any[]) {
// ...
}

因為裝飾器的目的只是對已有的對象進行修飾加強,所以你不能粗暴地將原始的對象直接替換成新的實現(當然你確實可以那樣粗暴的),那樣並不符合大多數應用場景。所以在進行替換時,先保存原始對象(這里原始對象是 foo 方法),然后在新的實現中對原始對象再進行調用,這樣來實現了對原始對象進行修飾,添加新的特性。

descriptor.value = function(...args: any[]) {
    console.log(`[g]before ${key} called`, args);
+    const result = original.apply(this, args);
    console.log(`[g]after ${key} called`, args);
    return result;
};

通過這種方式,多個裝飾器對被裝飾對象的修改可以層層傳遞下去,而不至於丟失。

下面把每個裝飾器接收到的屬性描述器打印出來:

function f() {
  console.log("f(): evaluated");
  return function(_target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
+    console.log("[f] receive descriptor:", original.toString());
    descriptor.value = function(...args: any[]) {
      console.log(`[f]before ${key} called`, args);
      const result = original.apply(this, args);
      console.log(`[f]after ${key} called`, args);
      return result;
    };
    console.log("f(): called");
    return descriptor;
  };
}

function g() {
console.log("g(): evaluated");
return function(_target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
+ console.log("[g] receive descriptor:", original.toString());
descriptor.value = function(...args: any[]) {
console.log([g]before ${key} called, args);
const result = original.apply(this, args);
console.log([g]after ${key} called, args);
return result;
};
console.log("g(): called");
return descriptor;
};
}

輸出結果:

[g] receive descriptor:
 function (count) {
        console.log("foo called " + count);
    }

[f] receive descriptor:
function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
console.log("[g]before " + key + " called", args);
var result = original.apply(this, args);
console.log("[g]after " + key + " called", args);
return result;
}

這里的示例中,先是 @g() 被調用,它接收到的 descriptor 就是原始的 foo 方法的屬性描述器,打印出其值便是原始的 foo 方法的方法體,

function (count) {
        console.log("foo called " + count);
    }

經過 @g() 處理后的屬性描述器傳遞給了下一個裝飾器 @f(),所以后者接收到的是經過處理后新的屬性描述器,即 @g() 返回的那個:

 function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            console.log("[g]before " + key + " called", args);
            var result = original.apply(this, args);
            console.log("[g]after " + key + " called", args);
            return result;
        }

然后將 @f()original 替換成上述代碼便是最終 @f() 返回的最終 foo 的樣子,大致是這樣的:

descriptor.value = function(...args: any[]) {
  console.log(`[f]before ${key} called`, args);

// g 開始
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
console.log("[g]before " + key + " called", args);

// foo 開始
console.log(</span>foo called <span class="pl-s1"><span class="pl-pse">${</span>count<span class="pl-pse">}</span></span><span class="pl-pds">);
// foo 結束

console.log("[g]after " + key + " called", args);
// g 結束

console.log(</span>[f]after <span class="pl-s1"><span class="pl-pse">${</span>key<span class="pl-pse">}</span></span> called<span class="pl-pds">, args);
return result;
};

所以最終的 foo 方法其實是 f(g(x)) 兩者嵌套組合的結果,像數學上的函數調用一樣。

總結

多個裝飾器運用於同一對象時,其求值和執行順序是相反的,

對於類似這樣的調用:

@f
@g
x
  • 求值順序是由上往下
  • 執行順序是由下往上

通常情況下我們只關心執行順序,除非是在編寫復雜的裝飾器工廠方法時。同時需要注意到,這里所指的裝飾器執行順序 是裝飾器本身被調用的順序,如果是裝飾方法,這和 descriptor.value 被執行的順序是兩碼事,后者的執行是層層嵌套的方式,聯想 Koa 中間件的洋蔥圈模型。

如果多個裝飾器中都對被裝飾對象有所修改,注意嵌套過程中修改被覆蓋的問題,如果不想要產生覆蓋,裝飾器中應該有對被裝飾對象保存副本並且調用,方法通過 fn.apply(),類則可通過返回一個新的但繼承自被裝飾對象的新類來實現,比如:

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}

console.log(new Greeter("world"));

這里覆蓋了被裝飾類的構造器,但其他未修改的部分仍是原來類中的樣子,因為這里返回的是一個 extends 后的新類。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM