Typescript Decorator - 5.0

110 阅读5分钟

阅读前须知: 需要了解typescript的基本操作, tsconfig的配置。 装饰器experimental的版本非常类似于Object.defineProperty(target, prop, descriptor),需要了解。

typescript装饰器在5.0版本不用experimental了,可以直接用。还需要注意如果用的ts 5的话,把这个配置注释掉,要不然还是跑的老的experimental的规则。

重要Note:

This new decorators proposal is not compatible with --emitDecoratorMetadata, and it does not allow decorating parameters. Future ECMAScript proposals may be able to help bridge that gap.

和老的experimental以及emitDecoratorMetadata不兼容,还不能在函数参数上加。

用下面代码结合ts playground切换不同版本看一下decorator函数都接收了什么参数:

function ComponentDeco(target:Function){
    console.log("component", target)
}

//for 5.0.+ use 
function ComponentDeco(...args:any){
    //target is the constructor of MyClass
    console.log("component", args)// 5+ 接收两个参数
}

function PropertyDeco(...args:any){
    console.log("property",args)
}

function MethodDeco(...args:any){
    console.log("method",args)

}

@ComponentDeco
class MyClass{
    
    @PropertyDeco
    name='hao'

    @MethodDeco
    SayHello(){
        console.log('Hello from inside class SayHello')
    }
}

4.9.5版本 + 开启experimentalDecorators 配置

输出

image.png

可以看到methodDeco第一个参数:函数本身,第二个参数:成员名字,第三个参数:这个成员的属性描述符

5.0.x版本。不用再设置experimentalDecorators了,正式在ts中可用。 装饰器接受的参数有比较大的改动。

不能修饰类外部的普通函数哦

www.typescriptlang.org/docs/handbo…

image.png

专注看一下类method装饰器的接收的参数,

function MethodDeco(...args:any){
    console.log("method",args)
}


class MyClass{
    name='hao'

    @MethodDeco
    SayHello(hello1:string, hello2:string){
        console.log('Hello from inside class SayHello',hello1,hello2,this.name)
    }
}

输出

image.png

第一个参数接收函数本身,第二个参数接收一个 ClassMethodDecoratorContext 类型的对象,这个对象里含有对这个method的各种描述。在5以前的版本,method接收三个参数,这里如果定义装饰器函数用了三个参数,就会报错了。

通常装饰器定义和用起来要:

function deco(toBeDecoedFunc):
   function DecoedFunc(args):
       //do something here
       toBeDecoedFunc(args)
   return DecoedFunc

@deco
function IwantToBeDecoedToDoSthElse(arg1){
  ...
}

从装饰器内部,会返回一个内部函数DecoedFunc,原函数IwantToBeDecoedToDoSthElse会被替换成它来执行。可以联想一下复合函数deco(origin(arg)),但是换了个写法。当然也可以这个DecoedFunc不调用原来的方法,做一些特殊的事情。

接下来,写一个简单的ts 5的装饰器,然后tsc编译一下看看执行过程:

function MethodDecoReturnFuncToCall(target:any, context:any){

    function decoedMethod(this:any,args:any){
        console.log('-----',this)
        target.call(this, args)
    }

    return decoedMethod
}


class MyClass2{
    

    name='hao'

    @MethodDecoReturnFuncToCall
    SayHello(hello1:string, hello2:string){
        console.log('Hello from inside class SayHello',hello1,hello2,this.name)
    }
}

(new MyClass2).SayHello('1','2')

经过MethodDecoReturnFuncToCall装饰后,调用SayHello的时候,就不再是调用SayHello了,实际上全权委托给了返回的decoedMethod(就是复合函数),它的this也变成了MyClass2,它也接收了SayHello调用时传入的参数'1'和'2'。

怎么回事呢,从tsc编译的结果(target:ES6)分析一下吧,只分析关键的几行:

  1. var MyClass2 = function ()这里 最后返回_a, 它的值是由后面的逗号表达式决定的,1:定义类,2: 设置decorator,具体是通过__esDecorate这个函数调用来实现的,传入decorator数组,我们这里就有一个所以就是往__esDecorate传入数组 [MethodDecoReturnFuncToCall]

  2. __esDecorate 函数里,最关键的

    var __esDecorate = ... {
             function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
             //装饰器函数返回值不是undefined和function类型就会报错,所以装饰器是能不返回值的,,
    
    
         var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
                //这一行把MethodDecoReturnFuncToCall执行了一下,它返回了decoedMethod作为result 
          ...
          ...
          else if (_ = accept(result)) { //注意装饰器函数不返回值的时候,descriptor就不会被改变,保持原样,这种时候还是调用了SayHello
             if (kind === "field") initializers.unshift(_);
             else descriptor[key] = _;
             //这里key='value'
             //_ = decoedMethod
           }
          if (target) Object.defineProperty(target, contextIn.name, descriptor);
              //最关键,
              // 将target(MyClass2),新增一个属性,contextIn.name (= Sayhello)
              // descriptor里的value是decoedMethod,
              // 所以就将MyClass2的Sayhello方法替换成了decoedMethod,
           // 注意这些都是在定义MyClass2的时候发生的,所以定义时,装饰器函数MethodDecoReturnFuncToCall就会被执行一次 
    }
    

由编译后的代码可见,类在定义时,会调用一下装饰器函数,并使用defineProperty,将被装饰的类方法替换成装饰器函数的返回函数。但装饰器函数不返回函数,且不返回undefined的话,调用这个被装饰的类方法就会报错了.但是如果装饰器函数返回类型undefined,那么调用这个类方法还是执行的原来的方法,不会被装饰器函数替换。

试验一下:


function MethodDecoReturnFuncToCall(target:any, context:any){

   return undefined
}


class MyClass2{
    
    name:string

    constructor(name:string){
        this.name=name
    }

    @MethodDecoReturnFuncToCall
    SayHello(hello1:string, hello2:string){
        console.log('Hello from inside class SayHello',hello1,hello2,this.name)
    }
}

(new MyClass2('haohao')).SayHello('1','2') 

//打印结果
// Hello from inside class SayHello 1 2 haohao

function MethodDecoReturnFuncToCall(target:any, context:any){

   return function(this:any, args:any){
        console.log('Hello from deco, not from SayHello method '+ this.name)
   }
}
//print
//Hello from deco, not from SayHello method haohao

ts 5 typed decorator

官方给的例子 Well-typed decorators

function loggedMethod<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
    const methodName = String(context.name);
    function replacementMethod(this: This, ...args: Args): Return {
        console.log(`LOG: Entering method '${methodName}'.`)
        const result = target.call(this, ...args);
        console.log(`LOG: Exiting method '${methodName}'.`)
        return result;
    }
    return replacementMethod;
}

class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    @loggedMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
}
const p = new Person("Ray");
p.greet();
function loggedMethod<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return

函数签名后的 <This, Args extends any[], Return> 定义本函数使用的三个泛型 第一个参数 (this: This, ...args: Args) => Return 一个函数类型(T, G)=> Y, ClassMethodDecoratorContext 类型,

可以简化一下写法,把那个函数类型用个单独的类型替代一下,增加一个泛型Fn

function loggedMethod<This, Args extends any[], Return, Fn extends (this: This, ...args: Args) => Return>(
    target: Fn,
    context: ClassMethodDecoratorContext<This, Fn>
)

using decorator factory

在装饰器函数外面再套一层函数,接收一些自定义的参数,来定制decorator

function lengthlimit(count:number){
    return function<This, Args extends [Array<any>, ...any[]], Return, Fn extends (this:This, ...args:Args)=>Return>(
        target:Fn,
        context: ClassMethodDecoratorContext<This, Fn>
    ){
        function replacementMethod(this:This, ...args:Args):Return {
            const firstArg = args[0] //Array<any>
            if(firstArg.length>count){
                throw new Error(`cannot call with >${count} items`)
            }
            return target.call(this, ...args)
        }
        return replacementMethod
    }
}

class TestClass{
    @lengthlimit(2)
    callWithNumLimit(arr:any[], info:string){
        return `${info}: ${arr.reduce((accrslt,currentvalue)=>accrslt+currentvalue,0)}` as unknown
    }
}


console.log((new TestClass).callWithNumLimit([1,2],'calling:'))

ts编译成js后,_callWithNumLimit_decorators = [limit(2)]; 来取出动态制造出的装饰器函数

使用ts 5的 field decorator

#need working on this .... pending on 0722

//ts5: for field decorator, target ===undefined
function Required(target: undefined, context: ClassFieldDecoratorContext) {

  return function(firstName:string){//这里就是接受的修饰的firstName属性

    return "decoed:"+firstName//覆盖掉,new User的时候firstName就会变成deoded
  }
}

//注意不要再自己写contruct了,会把装饰器返回的覆盖掉
class User {
  @Required
  firstName: string;

}

const user1 = new User();



//tbd

@withEmploymentDate
//@withEmploymentDateOnPrototype
class Manager {
    task: string = 'Simple task'
    project: string = 'Simple project'
    constructor(task:string){
        this.task=task
        console.log('Initializing the Manager class')
    }
}

console.log(new Manager('manager task'))

// function withEmploymentDateOnPrototype(arg: Function) {
//     arg.prototype.employmentDateOnPrototype = new Date().toISOString();
// }


//用原来的class extend出一个新的class返回回来,来实现调用构造函数
function withEmploymentDate<T extends { new(...args: any[]): {} }>(baseClass: T) { 
    console.log('Invoking decorator!!!')
    return class extends baseClass {
        //employmentDate = new Date().toISOString();
        constructor(...args: any[]) {
            super(...args);
            console.log('Adding employment date to ' + baseClass.name)
        }
    }
}