装饰器本质上提供了对被装饰对象 Property​ Descriptor 的操作,在运行时被调用。

因为对于同一对象来说,可同时运用多个装饰器,然后装饰器中又可对被装饰对象进行任意的修改甚至是替换掉实现,直观感觉会有一些主观认知上的错觉,需要通过代码来验证一下。

比如,假若每个装饰器都对被装饰对象的有替换,其结果会怎样?

多个装饰器的应用

通过编译运行以下示例代码并查看其结果可以得到一些直观感受:

  1. function f() {
  2. console.log("f(): evaluated");
  3. return function(_target: any, key: string, descriptor: PropertyDescriptor) {
  4. const original = descriptor.value;
  5. descriptor.value = function(...args: any[]) {
  6. console.log(`[f]before ${key} called`, args);
  7. const result = original.apply(this, args);
  8. console.log(`[f]after ${key} called`);
  9. return result;
  10. };
  11. console.log("f(): called");
  12. return descriptor;
  13. };
  14. }
  15.  
  16. function g() {

  17. console.log("g(): evaluated");

  18. return function(_target: any, key: string, descriptor: PropertyDescriptor) {

  19. const original = descriptor.value;

  20. descriptor.value = function(...args: any[]) {

  21. console.log(</span>[g]before ${<span class="pl-smi">key</span>} called<span class="pl-pds">, args);

  22. const result = original.apply(this, args);

  23. console.log(</span>[g]after ${<span class="pl-smi">key</span>} called<span class="pl-pds">);

  24. return result;

  25. };

  26. console.log("g(): called");

  27. return descriptor;

  28. };

  29. }
  30.  
  31. class C {

  32. @f()

  33. @g()

  34. foo(count: number) {

  35. console.log(</span>foo called ${<span class="pl-smi">count</span>}<span class="pl-pds">);

  36. }

  37. }
  38.  
  39. const c = new C();

  40. c.foo(0);

  41. c.foo(1);

先放出执行结果:

  1. f(): evaluated
  2. g(): evaluated
  3. g(): called
  4. f(): called
  5. [f]before foo called [ 0 ]
  6. [g]before foo called [ 0 ]
  7. foo called 0
  8. [g]after foo called [ 0 ]
  9. [f]after foo called [ 0 ]
  10. [f]before foo called [ 1 ]
  11. [g]before foo called [ 1 ]
  12. foo called 1
  13. [g]after foo called [ 1 ]
  14. [f]after foo called [ 1 ]

下面来详细分析。

编译后的装饰器代码

首页看看编译后变成 JavaScript 的代码,毕竟这是实际运行的代码:

编译后的代码
  1. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  2. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  3. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  4. 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;
  5. return c > 3 && r && Object.defineProperty(target, key, r), r;
  6. };
  7. var __metadata = (this && this.__metadata) || function (k, v) {
  8. if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
  9. };
  10. function f() {
  11. console.log("f(): evaluated");
  12. return function (_target, key, descriptor) {
  13. var original = descriptor.value;
  14. descriptor.value = function () {
  15. var args = [];
  16. for (var _i = 0; _i < arguments.length; _i++) {
  17. args[_i] = arguments[_i];
  18. }
  19. console.log("[f]before " + key + " called", args);
  20. var result = original.apply(this, args);
  21. console.log("[f]after " + key + " called", args);
  22. return result;
  23. };
  24. console.log("f(): called");
  25. return descriptor;
  26. };
  27. }
  28. function g() {
  29. console.log("g(): evaluated");
  30. return function (_target, key, descriptor) {
  31. var original = descriptor.value;
  32. descriptor.value = function () {
  33. var args = [];
  34. for (var _i = 0; _i < arguments.length; _i++) {
  35. args[_i] = arguments[_i];
  36. }
  37. console.log("[g]before " + key + " called", args);
  38. var result = original.apply(this, args);
  39. console.log("[g]after " + key + " called", args);
  40. return result;
  41. };
  42. console.log("g(): called");
  43. return descriptor;
  44. };
  45. }
  46. var C = /** @class */ (function () {
  47. function C() {
  48. }
  49. C.prototype.foo = function (count) {
  50. console.log("foo called " + count);
  51. };
  52. __decorate([
  53. f(),
  54. g(),
  55. __metadata("design:type", Function),
  56. __metadata("design:paramtypes", [Number]),
  57. __metadata("design:returntype", void 0)
  58. ], C.prototype, "foo", null);
  59. return C;
  60. }());
  61. var c = new C();
  62. c.foo(0);
  63. c.foo(1);

先看经过 TypeScript 编译后的代码,重点看这一部分:

  1. var C = /** @class */ (function () {
  2. function C() {
  3. }
  4. C.prototype.foo = function (count) {
  5. console.log("foo called " + count);
  6. };
  7. __decorate([
  8. f(),
  9. g(),
  10. __metadata("design:type", Function),
  11. __metadata("design:paramtypes", [Number]),
  12. __metadata("design:returntype", void 0)
  13. ], C.prototype, "foo", null);
  14. return C;
  15. }());

tslib 中装饰器的实现

其中 __decorate 为 TypeScript 经 tslib 提供的 Decorator 实现,其源码为:

tslib/tslib.js(经过格式化)

  1. var __decorate =
  2. (this && this.__decorate) ||
  3. function(decorators, target, key, desc) {
  4. var c = arguments.length,
  5. r =
  6. c < 3
  7. ? target
  8. : desc === null
  9. ? (desc = Object.getOwnPropertyDescriptor(target, key))
  10. : desc,
  11. d;
  12. if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
  13. r = Reflect.decorate(decorators, target, key, desc);
  14. else
  15. for (var i = decorators.length - 1; i >= 0; i--)
  16. if ((d = decorators[i]))
  17. r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  18. return c > 3 && r && Object.defineProperty(target, key, r), r;
  19. };

装饰器的执行顺序

配合编译后代码和这里装饰器的实现来看,进一步之前了解到的关于装饰器被求值和执行的顺序,

源码中应用装饰器的地方:

  1. @f()
  2. @g()
  3. foo(count: number) {
  4. console.log(`foo called ${count}`);
  5. }

然后这里的 @f() @g() 按照该顺序传递给了 __decorate 函数,

  1. __decorate(
  2. [
  3. + f(),
  4. + g(),
  5. __metadata("design:type", Function),
  6. __metadata("design:paramtypes", [Number]),
  7. __metadata("design:returntype", void 0)
  8. ],
  9. C.prototype,
  10. "foo",
  11. null
  12. );

然后在 __decorate 函数体中,对传入的 decorators 从数据最后开始,取出装饰器函数顺次执行,

  1. var __decorate =
  2. (this && this.__decorate) ||
  3. function(decorators, target, key, desc) {
  4. var c = arguments.length,
  5. r =
  6. c < 3
  7. ? target
  8. : desc === null
  9. ? (desc = Object.getOwnPropertyDescriptor(target, key))
  10. : desc,
  11. d;
  12. if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
  13. r = Reflect.decorate(decorators, target, key, desc);
  14. else
  15. + for (var i = decorators.length - 1; i >= 0; i--)
  16. if ((d = decorators[i]))
  17. r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  18. return c > 3 && r && Object.defineProperty(target, key, r), r;
  19. };

其中 r 便是装成器的返回,会被当作被装饰对象的新的属性描述器(Property Descriptor)来重新定义被装饰的对象:

  1. Object.defineProperty(target, key, r)

所以,像示例代码中多个装饰器均对被装饰对象有修改,原则上和多次调用 Object.defineProperty() 相当。

Object.defineProperty()

而调用 Object.defineProperty() 的结果是后面的会覆盖前面的,比如来看这里一个简单的示例:

  1. const obj = {};
  2.  
  3. Object.defineProperty(obj, "foo", {

  4. configurable: true,

  5. value: function() {

  6. console.log("1");

  7. }

  8. });
  9.  
  10. Object.defineProperty(obj, "foo", {

  11. value: function() {

  12. console.log("2");

  13. }

  14. });
  15.  
  16. 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.

回到本文开头的示例,为了进一步验证,可通过将运用装饰之后的属性描述器打印出来:

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

输出结果为:

  1. function () {
  2. var args = [];
  3. for (var _i = 0; _i < arguments.length; _i++) {
  4. args[_i] = arguments[_i];
  5. }
  6. console.log("[f]before " + key + " called", args);
  7. var result = original.apply(this, args);
  8. console.log("[f]after " + key + " called", args);
  9. return result;
  10. }

那么这里引出另一个问题,通过装饰器重复定义同一属性时,并没有显式返回一个 configurable:true 的对象,那为何在运用多个装饰器重复定义时没报错。

装饰器入参中的 descriptor

答案就只有一个,那就是装饰器传入的 descriptor 已经是 configurabletrue 的状态。

为了验证,只需要在 @f()@g() 任意一个装饰器中将 descriptor 打印出来即可。

  1. function g() {
  2. console.log("g(): evaluated");
  3. return function(_target: any, key: string, descriptor: PropertyDescriptor) {
  4. + console.log(descriptor)
  5. const original = descriptor.value;
  6. descriptor.value = function(...args: any[]) {
  7. console.log(`[g]before ${key} called`, args);
  8. const result = original.apply(this, args);
  9. console.log(`[g]after ${key} called`, args);
  10. return result;
  11. };
  12. console.log("g(): called");
  13. return descriptor;
  14. };
  15. }

输出的 descriptor

  1. {
  2. value: [Function],
  3. writable: true,
  4. enumerable: true,
  5. configurable: true
  6. }

这便是最终运行时会执行的 foo 方法真身。

可以看到确实是最后生效的装饰器确实是后运用的 @f()。因此你确实可以这么理解多个装饰器的重叠应用为,那一切都还说得通,就是 后运用的装饰器中 对被装饰对象的替换 会覆盖掉 先运用的装饰器 对被装饰对象的替换。

But,

这解释不了它的输出结果:

  1. f(): evaluated
  2. g(): evaluated
  3. g(): called
  4. f(): called
  5. [f]before foo called [ 0 ]
  6. [g]before foo called [ 0 ]
  7. foo called 0
  8. [g]after foo called
  9. [f]after foo called
  10. [f]before foo called [ 1 ]
  11. [g]before foo called [ 1 ]
  12. foo called 1
  13. [g]after foo called
  14. [f]after foo called

装饰器嵌套

原因就在于这句代码:

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

因为这句,@f()@g() 便不是简单的覆盖关系,而是形成了嵌套关系。

这里 originaldescriptor.value,即装饰器传入的 descriptor 的一个副本。我们在进行覆盖前保存了一下原方法的副本,

  1. // 保存原始的被装饰对象
  2. const original = descriptor.value;
  3.  
  4. // 替换被装饰对象

  5. descriptor.value = function(...args: any[]) {

  6. // ...

  7. }

因为装饰器的目的只是对已有的对象进行修饰加强,所以你不能粗暴地将原始的对象直接替换成新的实现(当然你确实可以那样粗暴的),那样并不符合大多数应用场景。所以在进行替换时,先保存原始对象(这里原始对象是 foo 方法),然后在新的实现中对原始对象再进行调用,这样来实现了对原始对象进行修饰,添加新的特性。

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

通过这种方式,多个装饰器对被装饰对象的修改可以层层传递下去,而不至于丢失。

下面把每个装饰器接收到的属性描述器打印出来:

  1. function f() {
  2. console.log("f(): evaluated");
  3. return function(_target: any, key: string, descriptor: PropertyDescriptor) {
  4. const original = descriptor.value;
  5. + console.log("[f] receive descriptor:", original.toString());
  6. descriptor.value = function(...args: any[]) {
  7. console.log(`[f]before ${key} called`, args);
  8. const result = original.apply(this, args);
  9. console.log(`[f]after ${key} called`, args);
  10. return result;
  11. };
  12. console.log("f(): called");
  13. return descriptor;
  14. };
  15. }
  16.  
  17. function g() {

  18. console.log("g(): evaluated");

  19. return function(_target: any, key: string, descriptor: PropertyDescriptor) {

  20. const original = descriptor.value;

  21. + console.log("[g] receive descriptor:", original.toString());

  22. descriptor.value = function(...args: any[]) {

  23. console.log([g]before ${key} called, args);

  24. const result = original.apply(this, args);

  25. console.log([g]after ${key} called, args);

  26. return result;

  27. };

  28. console.log("g(): called");

  29. return descriptor;

  30. };

  31. }

输出结果:

  1. [g] receive descriptor:
  2. function (count) {
  3. console.log("foo called " + count);
  4. }
  5.  
  6. [f] receive descriptor:

  7. function () {

  8. var args = [];

  9. for (var _i = 0; _i < arguments.length; _i++) {

  10. args[_i] = arguments[_i];

  11. }

  12. console.log("[g]before " + key + " called", args);

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

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

  15. return result;

  16. }

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

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

经过 @g() 处理后的属性描述器传递给了下一个装饰器 @f(),所以后者接收到的是经过处理后新的属性描述器,即 @g() 返回的那个:

  1. function () {
  2. var args = [];
  3. for (var _i = 0; _i < arguments.length; _i++) {
  4. args[_i] = arguments[_i];
  5. }
  6. console.log("[g]before " + key + " called", args);
  7. var result = original.apply(this, args);
  8. console.log("[g]after " + key + " called", args);
  9. return result;
  10. }

然后将 @f()original 替换成上述代码便是最终 @f() 返回的最终 foo 的样子,大致是这样的:

  1. descriptor.value = function(...args: any[]) {
  2. console.log(`[f]before ${key} called`, args);
  3.  
  4. // g 开始

  5. var args = [];

  6. for (var _i = 0; _i < arguments.length; _i++) {

  7. args[_i] = arguments[_i];

  8. }

  9. console.log("[g]before " + key + " called", args);
  10.  
  11. // foo 开始

  12. 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">);

  13. // foo 结束
  14.  
  15. console.log("[g]after " + key + " called", args);

  16. // g 结束
  17.  
  18. 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);

  19. return result;

  20. };

所以最终的 foo 方法其实是 f(g(x)) 两者嵌套组合的结果,像数学上的函数调用一样。

总结

多个装饰器运用于同一对象时,其求值和执行顺序是相反的,

对于类似这样的调用:

  1. @f
  2. @g
  3. x
  • 求值顺序是由上往下
  • 执行顺序是由下往上

通常情况下我们只关心执行顺序,除非是在编写复杂的装饰器工厂方法时。同时需要注意到,这里所指的装饰器执行顺序 是装饰器本身被调用的顺序,如果是装饰方法,这和 descriptor.value 被执行的顺序是两码事,后者的执行是层层嵌套的方式,联想 Koa 中间件的洋葱圈模型。

如果多个装饰器中都对被装饰对象有所修改,注意嵌套过程中修改被覆盖的问题,如果不想要产生覆盖,装饰器中应该有对被装饰对象保存副本并且调用,方法通过 fn.apply(),类则可通过返回一个新的但继承自被装饰对象的新类来实现,比如:

  1. function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
  2. return class extends constructor {
  3. newProperty = "new property";
  4. hello = "override";
  5. }
  6. }
  7.  
  8. @classDecorator

  9. class Greeter {

  10. property = "property";

  11. hello: string;

  12. constructor(m: string) {

  13. this.hello = m;

  14. }

  15. }
  16.  
  17. console.log(new Greeter("world"));

这里覆盖了被装饰类的构造器,但其他未修改的部分仍是原来类中的样子,因为这里返回的是一个 extends 后的新类。

TypeScript 装饰器的执行原理的更多相关文章

  1. 从C#到TypeScript - 装饰器

    总目录 从C#到TypeScript - 类型 从C#到TypeScript - 高级类型 从C#到TypeScript - 变量 从C#到TypeScript - 接口 从C#到TypeScript ...

  2. 基于TypeScript装饰器定义Express RESTful 服务

    前言 本文主要讲解如何使用TypeScript装饰器定义Express路由.文中出现的代码经过简化不能直接运行,完整代码的请戳:https://github.com/WinfredWang/expre ...

  3. TypeScript装饰器(decorators)

    装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上,可以修改类的行为. 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被 ...

  4. Angular 个人深究(一)【Angular中的Typescript 装饰器】

    Angular 个人深究[Angular中的Typescript 装饰器] 最近进入一个新的前端项目,为了能够更好地了解Angular框架,想到要研究底层代码. 注:本人前端小白一枚,文章旨在记录自己 ...

  5. python 中多个装饰器的执行顺序

    python 中多个装饰器的执行顺序: def wrapper1(f1): print('in wrapper1') def inner1(*args,**kwargs): print('in inn ...

  6. typescript装饰器 方法装饰器 方法参数装饰器 装饰器的执行顺序

    /* 装饰器:装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,属性或参数上,可以修改类的行为. 通俗的讲装饰器就是一个方法,可以注入到类.方法.属性参数上来扩展类.属性.方法.参数的功能. 常 ...

  7. python 装饰器、递归原理、模块导入方式

    1.装饰器原理 def f1(arg): print '验证' arg() def func(): print ' #.将被调用函数封装到另外一个函数 func = f1(func) #.对原函数重新 ...

  8. TypeScript 装饰器

    装饰器(Decorators)可用来装饰类,属性,及方法,甚至是函数的参数,以改变和控制这些对象的表现,获得一些功能. 装饰器以 @expression 形式呈现在被装饰对象的前面或者上方,其中 ex ...

  9. python3-多装饰器的执行顺序

    [例]: def dec1(func): print("HHHA:0====>") def one(): print("HHHA:0.1====>" ...

随机推荐

  1. VS2019 开发Django(九)------内置模板和过滤器

    导航:VS2019开发Django系列 紧接上篇,继续介绍Django中的模板,考虑可能篇幅过长,所以分为两部分来讲,今天的主要内容: 1)内置模板和过滤器 母版,继承关系.头部导航和页脚,是需要与其 ...

  2. 日志介绍与rsyslogd服务管理与配置

    一.日志简介 1.日志相关服务介绍 在 CentOS 6.x 中日志服务使用 rsyslogd 服务,rsyslogd 具有以下特点: 基于 TCP 网络协议传输日志信息 更安全的网络传输方式 有日志 ...

  3. SpringMVC框架之第一篇

    2.SpringMVC介绍 2.1.SpringMVC是什么 SpringMVC是Spring组织下的一个表现层框架.和Struts2一样.它是Spring框架组织下的一部分.我们可以从Spring的 ...

  4. Supermap/Cesium 开发心得----动态散点图(波纹点/涟漪点)

    在二维开发中,openlayers4 入门开发系列结合 echarts4 实现散点图,下图是GIS之家的效果图,那么在三维中,则可借助Entity来变相构造下图的效果. 思路: 构造实体ellipse ...

  5. 63-容器在 Weave 中如何通信和隔离?

    上一节我们分析了 Weave 的网络结构,今天讨论 Weave 的连通和隔离特性. 首先在host2 执行如下命令: weave launch 192.168.0.44 这里必须指定 host1 的 ...

  6. ImportError: No module named flask 导包失败,Python3重新安装Flask模块

    在部署环境过程中,通过pip install -r requirements.txt安装包,结果启动项目时总是报错,显示没有flask模块,通过pip install flask还是不行,于是下载fl ...

  7. .NET Core和无服务器框架

    无服务器框架是一个云提供商无关的工具包,旨在帮助构建,管理和部署无服务器组件的操作,以实现完整的无服务器架构或不同功能即服务(FaaS).无服务器框架的主要目标是为开发人员提供一个界面,该界面抽象出云 ...

  8. HTTP与WWW服务

    1.查看本地DNS缓存 ipconfig /displaydns #显示DNS缓存内容ipconfig /flushdns #清除DNS缓存 2.查看本地hosts. C:\Windows\Syste ...

  9. 1.编译spring源码

    本文是作者原创,版权归作者所有.若要转载,请注明出处 下载spring源码,本文用的是版本如下: springframework 5.1.x,   IDE工具idea 2019.2.3    JAVA ...

  10. WebShell代码分析溯源(八)

    WebShell代码分析溯源(八) 一.一句话变形马样本 <?php $e=$_REQUEST['e'];$arr= array('test', $_REQUEST['POST']);uasor ...