【从TS到JS源码解析】TS装饰器

188 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情

前言

最近在学习TS,感觉学习TS的最好方法就是对照编译之后的JS源码进行比较学习,这样就容易理解TS语法的功能~~~

这篇文章就讲一讲TS中的装饰器吧,若要启用实验性的装饰器特性,必须tsconfig.json里启用experimentalDecorators编译器选项!!!

装饰器模式

装饰器模式重点在于装饰二字,在我们平时也会经常遇到这样的场景,举一个小例子,你新买一个手机,因为怕手机会被摔坏,这个时候你要去再给他配一个手机壳,那么这个手机壳就是用来装饰手机,也就是手机的装饰器!!!

我们可以通过上面的手机例子得出几个关于装饰器的结论:

  • 被装饰的目标就算失去了装饰器也可以正常使用功能;(手机离开手机壳也可以正常使用)
  • 被装饰的目标和装饰器是多对多的关系;(手机可以使用手机壳、钢化膜,手机壳也可以用在多个手机上使用)

使用场景

你应该遇到过这样的场景,当你要在旧代码进行添加新功能时,你发现旧代码中定义了Person类中的属性和方法不满足新的功能和需求,你需要对Person类进行扩充

class Person {
    name;
    getName() {
        return this.name
    }
}

如果让你来扩充,你会怎样进行修改喃?是对Person类的属性和方法重新定义?

class Person {
    name;
    sex;
    getName() {
        return this.name
    }
    getSex() {
        return this.name
    }
}

但是如果有一天产品经理说这个新功能要舍去了,你又把Person类再次定义回去嘛!!!

这显然是很糟糕的写法,我们应该遵守开放封闭原则,不影响旧的Person类的情况下添加新的属性和方法!!!

class Person {
    name;
    getName() {
        return this.name
    }
}

// 补充代码
Person.prototype.sex = "未知"
Person.prototype.getSex = function () {
    return this.sex
}

这样对Person类进行补充,既不影响以前Person类,又对当前Person类进行了补充,这就是装饰器模式

装饰器是对目标补充,它能够作用于类声明方法属性或者参数


TS中装饰器

  • 装饰器是一个函数,通过*@函数名*使用
  • 根据装饰器传入的参数分类:
    • 普通装饰器(无法传参)
    • 装饰器工厂(可以传参)
  • 根据装饰器的装饰的目标分类:
    • 类装饰器
    • 方法装饰器
    • 属性装饰器
    • 访问器装饰器
    • 参数装饰器
  • 多个装饰器执行顺序按照从下到上

注:如果你对装饰器的基础有了解,也可以直接跳转到源码学习

装饰器参数

普通装饰器

function addRun(target: any) {
    target.prototype.run = function () {
        console.log("在奔跑!!!");
    }
}

@addRun
class Person { }
(new Person() as any).run()    // 在奔跑!!!

装饰器工厂

function addRun(area: string) {
    // 外部函数用来接受参数,再返回装饰器函数
    return function (target: any) {
        target.prototype.run = function () {
            // 使用外部函数接受的参数
            console.log(`在${area}奔跑!!!`);
        }
    }
}

@addRun("草地")
class Person { }
(new Person() as any).run()    // 在草地奔跑!!!

这里主要对比普通装饰器装饰器工厂装饰器的函数传参,后面会通过编译之后的JS进行分析

TS无法识别问题

在上的代码中之所以会将new Person()对象断言为any,是因为TS无法识别通过装饰器添加的属性和方法,会有报错提示!!!

也可以通过接口去定义Person的类型

function addRun(target: any) {
    target.prototype.run = function () {
        console.log("在奔跑!!!");
    }
}

interface Person {
    run(): void
}
@addRun
class Person { }
new Person().run()    // 在奔跑!!!

装饰器分类

类装饰器

  • 参数:
    • target:类的构造函数
  • 返回值:使用返回的类的构造函数替换原来的类
function addRun(target: any) {
    // 返回一个新的构造函数,替代原来的构造函数
    return class extends target {
        area: string = "草地"
        run() {
            console.log(`在${this.area}奔跑!!!`);
        }
    }
}

@addRun
class Person { }
(new Person() as any).run()    // 在奔跑!!!

方法装饰器

  • 参数
    • target:对于静态方法来说是类的构造函数,对于实例方法是类的原型对象
    • fieldName:方法名称
    • descriptor:方法的属性描述符
  • 返回值:根据返回的属性描述符重新定义方法的属性描述符
修饰实例方法
function addRun(target: any, fieldName: string, descriptor: PropertyDescriptor) {
    // target: 类的原型对象
    // 最初的run方法
    let _run = target[fieldName]
    return {
        // 给run方法添加补充
        value(...arg) {
            console.log("开始运动了!!!");
            _run.apply(this, arg)
        }
    }
}

class Person {
    @addRun
    run() {
        console.log("在奔跑!!!");
    }
}
new Person().run()
/**
 * 开始运动了!!!
 * 在奔跑!!!
 */
修饰静态成员
function addRun(target: any, fieldName: string, descriptor: PropertyDescriptor) {
    // target: 类的构造函数
    // 最初的run方法
    let _run = target[fieldName]
    return {
        // 给run方法添加补充
        value(...arg) {
            console.log("开始运动了!!!");
            _run.apply(this, arg)
        }
    }
}

class Person {
    @addRun
    static run() {
        console.log("在奔跑!!!");
    }
}
Person.run()
/**
 * 开始运动了!!!
 * 在奔跑!!!
 */

访问器装饰器

访问器本质和方法时一样的,所以参数和返回值也是一样的

function addArea(target: any, fieldName: string, descriptor: PropertyDescriptor) {
    // target:类的构造函数
    let _value = target[fieldName]
    return {
        get(...arg): string {
            return `在${_value}奔跑!!!`
        }
    }
}

class Person {
    private static _area: string = "草地"

    @addArea
    static get run() {
        return this._area
    }
}
console.log(Person.run);  // 在草地奔跑!!!

属性修饰器

先了解一下属性和方法的区别

class Person {
    name: string = "李白"
    run() {
        console.log("在奔跑!!!");
    }
}

上面代码换成ES5的代码

function Person() {
    this.name = "李白"
}
Person.prototype.run = function () {
    console.log("在奔跑!!!");
}

属性是在实例化的才会被赋值的,所以在类声明阶段,也就拿不到属性的描述符,修饰器也就没有第三个参数,返回值也就没什么意义了!!!

静态属性虽然会在类声明阶段就赋值,但是TS可能为了同一参数,在第三个参数也没有赋值


  • 参数

    • target:对于静态属性来说是类的构造函数,对于实例属性是类的原型对象

    • fieldName:属性名称

  • 返回值:无

function show(target: any, fieldName: string) { }

class Person {
    @show
    name: string;
    @show
    static age: number;
}

参数装饰器

函数参数和属性修饰符是一样的,都在声明阶段没有赋值,所以无法拿到属性的描述符

  • 参数
    • target:对于静态属性来说是类的构造函数,对于实例属性是类的原型对象
    • fieldName:函数的名称
    • paramIndex:参数在函数参数列表中的索引
function show(target: any, fieldName: string, paramIndex: number) {
    // target:类的原型对象
    // fieldName:函数的名称
    // paramIndex: 参数在函数参数列表中的索引
}

class Person {
    run(@show area: string) { }
}

装饰器执行顺序

function decorator1(target: any) {
    console.log("decorator1执行了!!!");
}
function decorator2(target: any) {
    console.log("decorator2执行了!!!");
}

@decorator1
@decorator2
class Person { }
/**
* decorator2执行了!!!
* decorator1执行了!!!
*/

执行顺序是decorator2 => decorator1,也就是先执行靠近Person的装饰器,也可以理解为

decorator1(decorator2(Person))

装饰器执行顺序:自下向上

源码学习

以类装饰器为例子吧

function addRun(target: any) { }
function addName(target: any) { }

@addRun
@addName
class Person { }

看一下编译之后的JS的代码:

image-20230117114727635

装饰器是通过调用__decorate函数挂载到被装饰目标上的

注:这里默认Reflect不存在,方便理解__decorate的实现原理

/**
 * 装饰器挂载到目标对象上
 *
 * @param {Array<Function>} decorators              // 装饰器数组
 * @param {Object} target                           // 目标对象( 类的构造函数、类的原型对象)
 * @param {string} [key]                            // 名称(属性名称、方法名称)     
 * @param {(PropertyDescriptor | null)} [desc]      // 属性描述符
 * @return {(Object | void)}                        
 */
function __decorate(decorators, target, key, desc) {

    let paramLen = arguments.length // 获取参数的长度
    let result = null // 定义函数的返回值
    if (paramLen < 3) {
        result = target
    } else {
        if (desc === null) {
            desc = Object.getOwnPropertyDescriptor(target, key) // 获取属性描述符
        }
        result = desc
    }

    /**
     * 执行装饰器函数,挂载到目标对象上
     */
    let decorator = null
    for (var i = decorators.length - 1; i >= 0; i--) {
        if (decorator = decorators[i]) {
            let _result = null
            if (paramLen < 3) {
                _result = decorator(result)
            } else if (paramLen > 3) {
                _result = decorator(target, key, result)
            } else {
                _result = decorator(target, key)
            }
            result = _result || result
        }
    }

    /**
     * 修改属性描述符
     */
    if (paramLen > 3 && result) {
        Object.defineProperty(target, key, r)
    }

    return result;
}


// 装饰器
function addRun(target) { }
function addName(target) { }

//被装饰目标
let Person = class Person { }
// 挂载装饰器
Person = __decorate([
    addRun,
    addName
], Person);
  • __decorate的函数主体是不会改变的,之所以我们实现了不同分类的装饰器,是通过给__decorate函数传入不同的参数,在__decorate内部就行分类判断
  • 装饰器所需的参数是__decorate的函数主体传入

以源码的角度理解一下TS中的装饰器吧

装饰器分类

类装饰器
function addRun(target) { }							// 装饰器
let Person = class Person { }						// 被装饰的类

// __decorate接受的参数为2
// 在执行装饰器时,执行的是:  _result = decorator(result) ,装饰器接受1个参数
// __decorate函数的返回值会重新赋值给被装饰的类,用于覆盖以前定义的类
Person = __decorate([	
    addRun
], Person);
方法装饰器
  • 实例方法
function addRun(target, fieldName, descriptor) { }   // 装饰器
class Person {			
    run() { }									 // 被装饰的实例方法
}

// __decorate接受的参数为4
// 在执行装饰器时,执行的是:  _result = decorator(target, key, result) ,装饰器接受3个参数
// __decorate函数的返回值不会被使用,
// 参数长度为4,会执行Object.defineProperty(target, key, r) ,重新定义被装饰的方法的属性描述符
__decorate([
    addRun
], Person.prototype, "run", null);
  • 静态方法
function addRun(target, fieldName, descriptor) { }	// 装饰器		
class Person {
    static run() { }							 // 被装饰的静态方法
}

// 和实例方法是一样的,只是__decorate的第二个参数是类的构造函数
__decorate([
    addRun
], Person, "run", null);
访问器修饰器

和方法装饰器的执行是一样的,这里就不细讲了

function addArea(target, fieldName, descriptor) { }			// 装饰器		
class Person {
    static get run() {									 // 被装饰的访问器
        return "草地";
    }
}

// __decorate接受的参数为4
__decorate([
    addArea
], Person, "run", null);
属性修饰器
  • 修饰实例属性
function show(target, fieldName) { }					// 装饰器	
class Person {
    constructor() {
        this.name = "李白";							// 被装饰的属性
    }
}

// void 0的值是undefined,不会计入实参个数,__decorate接受的参数为3
// 在执行装饰器时,执行的是:  _result = decorator(target, key) ,装饰器接受2个参数
// __decorate函数的返回值不会被使用,
// 参数长度为3,会执行Object.defineProperty(target, key, r) ,重新定义被装饰的属性的属性描述符
__decorate([
    show
], Person.prototype, "name", void 0);
  • 修饰静态属性
function show(target, fieldName) { }		  // 装饰器	
class Person { }
Person.age = 18;							// 被装饰的属性

// 和修饰实例属性是一样的,只是__decorate的第二个参数是类的构造函数
__decorate([
    show
], Person, "age", void 0);
参数修饰器

参数装饰器和其他的装饰器不一样,对装饰器包装了一个__param,这个方法是为了传递索引

var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
function show(target, fieldName, paramIndex) { }
class Person {
    run(area) { }
}

// 和方法装饰器很相似,区别在于执行装饰器时:_result = decorator(target, key, result)   result是索引
__decorate([
    __param(0, show)
], Person.prototype, "run", null);

装饰器参数

function add(a, b) {
    return function (target) {
        target.sum = a + 1;
    };
}
let Person = class Person { }

// 先执行add函数获取返回值
Person = __decorate([
    add(1, 2)             // 返回装饰器函数
], Person);

装饰器执行顺序

function test1() {
    console.log("test1函数执行");
    return function (target) {
        console.log("test1装饰器执行");
    }
}
function test2() {
    console.log("test2函数执行");
    return function (target) {
        console.log("test2装饰器执行");
    }
}

@test1()
@test2()
class Person { }

执行结果是什么?

test1函数执行
test2函数执行
test2装饰器执
test1装饰器执行

答对了嘛?再配合一下JS代码

function test1() {
    console.log("test1函数执行");
    return function test1_decorator(target) {
        console.log("test1装饰器执行");
    };
}
function test2() {
    console.log("test1函数执行");
    return function test2_decorator(target) {
        console.log("test1装饰器执行");
    };
}
let Person = class Person {
};

Person = __decorate([
    test1(),
    test2()
], Person);

先讨论一下装饰器工厂如何执行

  1. 先执行test1函数返回test1_decorator装饰器函数

  2. 再执行test2函数返回test2_decorator装饰器函数

  3. __decorate参数变为:

    Person = __decorate([
        test1_decorator,
        test2_decorator
    ], Person);
    

得出结论,在给__decorate传入参数时,会先从上到下执行装饰器工厂的包装函数

再讨论一下__decorate主体执行装饰器的过程

 for (var i = decorators.length - 1; i >= 0; i--) {
     /** 执行装饰函数 **/
 }

decorators就是传入的装饰器数组,会发现在执行decorators数组时,是从后往前执行,也就是在执行装饰器时,是按照从下到上执行装饰器函数