Fast 中的 Decorator 在 typesciprt 及 js 中的差异

Fast 中的 Decorator 在 typesciprt 及 js 中的差异

结论

ts 中约定属性的装饰器语法只有两个参数type PropDecorator<T> = (target: T, key:string):void; 而在 js 中旧的装饰器的语法var type PropDecorator<T> = (target:T, propertyName:string, descriptor: ObjectDescriptor):ObjectDescriptor|void。对于 ts 而言,如果类的属性本身已经定义了 Descriptor, 那么装饰器的效果跟 js 中是一样的。如果属性本身并没有定义 descriptor,那么装饰器本身要么返回 descriptor,否则这个装饰器就不会体现在 descriptor 上面。
然而对于 babel 而言,首先,装饰器一定是体现在 descriptor 上面的,装饰器的体现必须是直接修改传递给装饰器的 descriptor 对象,或者装饰器返回新的 descriptor 对象。如果直接操作 property,则会被覆盖掉。其次装饰器的行为最终是体现在实例上面,而不是原型上面。

Typescript

ts 中的 decorator 只能作用于类实例的属性,方法,存取器;静态的属性,方法,存取器;构造函数;类本身

  • Decorator 的语法如下
type ClassDecoratorType<T extends new (...args:any[]):{}> = (classDef:T) => T | void;
type MethodOrAccessorDecoratorType = (target: PrototypeOfClass|ConstructorFuncOfClass, name: string, descriptor:ObjectPropertyDescriptor) => ObjectPropertyDescriptor | void;
type PropertyDecoratorType = (target: PrototypeOfClass|ConstructorFuncOfClass, name:string)=>void; // 没有第三个参数
type ParameterDecoratorType = (target: PrototypeOfClass|ConstructorFuncOfClass, name:string, index:number)=>void;  // 参数的Decorator只能适用于class Constructor/Method Parameter

js

目前 js 中关于 decorator 的提案处于第三阶段,我们只讨论第三阶段及前一个版本的使用。Decorator 语法是@functionName,可以作用于类,类的属性,类的方法,属性存取器

  • Decorator 本身就是一个函数,它的新语法如下:
type Decrotor = (
  value: Input,
  context: {
    kind: string;
    name: string | symbol;
    access: {
      get?(): unknown;
      set?(value: unknown): void;
    };
    private?: boolean;
    static?: boolean;
    addInitializer?(initializer: () => void): void;
  }
) => Output | void;

新版的装饰器,本身就是一个函数,它有两个参数,第一个是被修饰的对象的值(可能是 undefined),第二个是上下文。这个装饰器函数,要么返回新的被修饰的对象,要么不返回。我们也可以将定义一个函数返回上述的装饰器函数。

  • 当有多个装饰器函数的时候,执行顺序:
    • 计算装饰器值是从左到右,从上到下
    • 调用方法的装饰器函数
    • 调用类的装饰器

分别介绍一下装饰器的语法结构

// 类老的语法
type ClassDecoratorOld=(class:Function)=>void;
// 类的新语法
type ClassDecoratorNew=(value:Function,context:{
    kind:'class';
    name:string|undefined;
    addInitializer(initializer:()=>void):void;
})=>NewFunction|void

// 方法新语法  可以返回一个新函数或者不返回
type ClassMethodDecorator=(value:Function, context:{
    kind:'method';
    name: string| symbol;
    access:{get():unknow};
    static:boolean;
    private:boolean;
    addInitializer(initializer:()=>void):void;
})=>Function|void

// 属性|方法装饰器
type ClassPropertyDecorator=(target:Object, name:string, decorator:Descriptor)=>Descriptor;

// 属性装饰器新语法
type ClassFieldDecorator=(value:undefined,context:{
    kind:'field';
    name:string|symbol;
    access:{get():unknow,set(value:unknow):void};
    static:boolean;
    private:boolean;
}) => (initialValue:unknow)=>unknow | void;


// Get装饰器新语法
type ClassGetterDecorator = (value:Function, context:{
    kind:'getter';
    name: string | symbol;
    access: {get(): unknow};
    static: boolean;
    private: boolean;
    addInitializer(initializer: ()=>void):void;
}) => Function | void

// Set 装饰器新语法
type ClassSetterDecorator = (value:Function, context:{
    kind:'setter';
    name: string|symbol;
    access:{set(value:unknow):void};
    static:boolean;
    private:boolean;
    addInitializer(initializer: () => void):void;
}) => Function | void

类的装饰器(老的语法)

下面就是一个最简单的例子

// 老的语法
function testable(isTestable) {
  return function (target) {
    target.prototype.isTestable = isTestable;
  };
}

// 新的语法
function newTestable(isTestable) {
  return function (value, { kind, name }) {
    if (kind === "class") {
      return class extends value {
        constructor(...args) {
          super(...args);
          this.isTestable = isTestable;
        }
      };
    }
  };
}

// 方法的新的语法
function replace(func, { kind }) {
  return function (...args) {
    const pre = func.call(this, ...args);
    return pre;
  };
}

// 属性的语法
function readonly(target, name, descriptor) {
  return {
    ...descriptor, // Object.getOwnPropertyDescriptor(target,name)的返回值
    writable: false,
  };
}

@testable(true)
class MyClass {
  @readonly
  name: string;

  @replace
  greeting() {
    return `hello ${this.name}`;
  }
}
function log1(target, name, descriptor) {
  const originalFunc = descriptor.value;
  descriptor.value = function (...args) {
    console.log(`old decorator call`);
    return originalFunc.call(this, ...args);
  };
}

function log2(value) {
  return function (...args) {
    console.log(`new decorator call`);
    return value.call(this, ...args);
  };
}

// 很遗憾下面的代码在最新版的edge/chrome 里面无法执行
class Test {
  @log1
  fun1() {
    console.log("fund1");
  }

  @log2
  fun2() {
    console.log("func2");
  }
}

fast 中的 Decorator 为什么只能使用 ts-loader 进行编译,babel 转码的时候究竟怎么犯错了。以代码为例

// 这个是observable 的定义的关键点
// 在这个decorator 內部,我们直接在原型上定义了Property,因为typescript的语法中属性的decorator只能接收两个参数,没有第三个descriptor.
// 这个方式的弊端是class 的属性上的装饰器只有一个会起作用
function observable(target, name) {
  Object.defineProperty(target, name, {
    enumerable: true,
    get() {},
    set(value) {},
  });
}

class MyClass {
  @observable prop: number;
}
  • 上面的代码如果使用 tsc 进行转码,得到的代码关键点如下
var __decorator = function (decorators, target, key, desc) {
  // 这个地方是从对象原型本身去desciprtor,对于class 这个值是没有的
  let finaldesc =
    desc === null ? Object.getOwnPropertyDescriptor(target, key) : desc;
  for (let i = decorators.length - 1; i >= 0; i--) {
    finaldesc = decorators[i](target, key, finaldesc) || finaldesc;
  }
  // 所以finaldesc 这个对象是空的,是不会重新定义Property的,所以decorator里面的原型定义得到了保留
  if (finaldesc) {
    Object.defineProperty(target, key, finaldesc);
  }
};

class MyClass {
  constructor() {}
}

__decorator([observable], MyClass.prototype, "prop", void 0);
  • 如果我们使用 babel 来进行转码,babel 配置如下
const presets = [["@babel/preset-env"], ["@babel/preset-typescript"]];

const plugins = [
  ["@babel/plugin-proposal-decorators", { version: "legacy" }],
  ["@babel/plugin-proposal-class-properties"],
];

module.exports = {
  presets,
  plugins,
};

那么转码后的 js 关键点如下

let __descriptor;
var MyClass = function () {
  // 类的属性是定义在实例上的,而不是原型上,它会在构建实例的时候将属性装饰器整合到实例的属性中去
  Object.defineProperty(this, "prop", __descriptor);
};
__descriptor = __applyDecoratorDescriptor(
  MyClass.prototype,
  "prop",
  [observable],
  {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: null,
  }
);

// 这个地方同样是聚合。与ts不同的是,原型上的属性装饰器会聚合到原型上,同时,约定了,修改Descriptor 的途径要么就是直接修改,要么就是返回新的Descriptor对象
__applyDecoratorDescriptor = (target, key, decorators, initialDescriptor) => {
  let finaldes = initialdescriptor;
  for (let i = decorators.length - 1; i >= 0; i--) {
    finaldes = decorators[i](target, key, finaldes) || finaldes;
  }
  Object.defineProperty(target, key, finaldes);
};
posted @   kongshu  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示