博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
TypeScript 装饰器的执行原理
阅读量:4322 次
发布时间:2019-06-06

本文共 12701 字,大约阅读时间需要 42 分钟。

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

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

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

多个装饰器的应用

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

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(`[g]before ${
key} called`, args); const result = original.apply(this, args); console.log(`[g]after ${
key} called`); return result; }; console.log("g(): called"); return descriptor; };}class C { @f() @g() foo(count: number) { console.log(`foo called ${
count}`); }}const c = new C();c.foo(0);c.foo(1);

先放出执行结果:

f(): evaluatedg(): evaluatedg(): calledf(): 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 经 提供的 Decorator 实现,其源码为:

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

注意: 根据 ,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(): evaluatedg(): evaluatedg(): calledf(): 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(`foo called ${
count}`); // foo 结束 console.log("[g]after " + key + " called", args); // g 结束 console.log(`[f]after ${
key} called`, args); return result;};

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

总结

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

对于类似这样的调用:

@f@gx
  • 求值顺序是由上往下
  • 执行顺序是由下往上

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

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

function classDecorator
(constructor:T) { return class extends constructor { newProperty = "new property"; hello = "override"; }}@classDecoratorclass Greeter { property = "property"; hello: string; constructor(m: string) { this.hello = m; }}console.log(new Greeter("world"));

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

转载于:https://www.cnblogs.com/Wayou/p/typescript_decorator_under_the_hook.html

你可能感兴趣的文章
Velocity模版进行shiro验证
查看>>
新生舞会
查看>>
双倍回文(bzoj 2342)
查看>>
微软Coco Blockchain Framework:一键解决企业级区块链三大难题
查看>>
Azure 虚拟机诊断设置问题排查
查看>>
C++入门经典-例4.8-同名的全局变量和局部变量
查看>>
文章阅读报告 -- 自媒体时代的电子阅读
查看>>
python并行编程学习之并行计算存储体系结构
查看>>
Asp.net常用的51个代码(非常实用)
查看>>
深度学习中一些常用函数的偏导数
查看>>
解决离线Could not parse configuration:hibernate.cfg.xml错误
查看>>
关于Win7 x64下过TP保护(应用层)(转)
查看>>
6月7号
查看>>
JS五星级评分效果(类似与淘宝打分效果)
查看>>
JQuery的源码阅读
查看>>
css 背景色半透明 兼容各个浏览器ie6 ie8 火狐
查看>>
Zookeeper 扫盲
查看>>
工作8年的经验之谈
查看>>
在gem5的full system下运行 alpha编译的测试程序 running gem5 on ubuntu in full system mode in alpha...
查看>>
Android GPS应用:临近警告
查看>>