TypeScript 装饰器执行顺序与洋葱模型

在前面的学习中,我们已经分别掌握了类、方法、属性、参数这 4 种装饰器是如何使用的。但在真实的架构设计(如 NestJS 或 Angular 源码)中,一个类往往会被密密麻麻的装饰器所包裹。

这时候,一个严肃的问题就摆在了我们面前:谁先执行,谁后执行? 如果顺序搞反了,权限校验可能就会在日志记录之前发生,从而导致系统逻辑混乱。今天,我们就来彻底搞清楚装饰器的执行 “生命周期”。

TypeScript 不同类型装饰器的优先级

在 TypeScript 中,如果一个类中同时存在多种类型的装饰器,它们的执行并不是随意的,而是遵循一套从 “微观” 到 “宏观” 的严密顺序:

  • 实例成员:按在类中声明的先后顺序(从上到下)依次评估与执行。对于同一个方法而言,参数装饰器优先于方法装饰器执行。
  • 静态成员:同样按在类中声明的 “先后顺序” 依次评估与执行。对于同一个方法而言,也是参数优先于方法。
  • 构造函数:参数装饰器。
  • 类装饰器:永远最后执行。

简单来说,TypeScript 的策略是:先装修房间内部(按代码从上到下扫荡实例和静态成员),最后再装修整栋大楼()。

TypeScript 不同类型装饰器的优先级

TypeScript 同类型装饰器的 “洋葱模型”

在 TypeScript 中,当同一个目标(比如同一个方法)上贴了多个装饰器时,情况会变得更加有趣。TypeScript 采用了函数式编程中经典的 “洋葱模型(Onion Model)”,其核心逻辑是:“由外向内评估,由内向外执行”。

我们可以把装饰器工厂的求值过程想象成 “剥洋葱”,而把装饰器函数的实际执行想象成 “洋葱心的爆发”。

其执行规则如下:

  • 评估(Evaluation):装饰器工厂函数从 “上到下” 依次执行。
  • 调用(Call):装饰器真实的逻辑函数从 “下到上” 依次执行。

TypeScript 同类型装饰器的 “洋葱模型”

TypeScript 装饰器全家桶的生命周期

为了验证上述所有理论,我们编写一个包含所有装饰器类型的 “全能类”,并观察控制台的输出。

示例:全维度执行顺序测试(含静态成员)

// 1. 类装饰器工厂
function ClassDec(name: string) {
    console.log("评估类装饰器:", name);
    return (target: Function) => console.log("执行类装饰器:", name);
}

// 2. 属性装饰器工厂
function PropDec(name: string) {
    console.log("评估属性装饰器:", name);
    return (target: any, key: string) => console.log("执行属性装饰器:", name);
}

// 3. 方法装饰器工厂
function MethodDec(name: string) {
    console.log("评估方法装饰器:", name);
    return (target: any, key: string, desc: PropertyDescriptor) => console.log("执行方法装饰器:", name);
}

// 4. 参数装饰器工厂
function ParamDec(name: string) {
    console.log("评估参数装饰器:", name);
    return (target: any, key: string, index: number) => console.log("执行参数装饰器:", name);
}

@ClassDec("TopClass")
class TestExecutor {
    // 实例成员(最先执行)
    @PropDec("InstanceUsername")
    public name: string = "Jack";

    @MethodDec("OuterMethod")
    @MethodDec("InnerMethod")
    public saveData(@ParamDec("ParamFirst") id: number) {
        console.log("业务代码执行中...");
    }

    // 静态成员(在实例成员之后执行)
    @PropDec("StaticVersion")
    public static version: string = "1.0";
}

运行结果如下。

评估属性装饰器:InstanceUsername
执行属性装饰器:InstanceUsername
评估方法装饰器:OuterMethod
评估方法装饰器:InnerMethod
评估参数装饰器:ParamFirst
执行参数装饰器:ParamFirst
执行方法装饰器:InnerMethod
执行方法装饰器:OuterMethod
评估属性装饰器:StaticVersion
执行属性装饰器:StaticVersion
评估类装饰器:TopClass
执行类装饰器:TopClass

分析:

想要真正地掌握 TypeScript,小伙伴们应该反复阅读上面的运行结果,因为这是理解 TypeScript 架构的灵魂所在:

  • 实例优先于静态:我们可以清晰地看到,StaticVersion(静态属性)的评估与执行,严格排在了所有实例成员的后面.
  • 声明顺序决定同级优先级:虽然 InstanceUsername(属性)和 OuterMethod(方法)同属实例成员,但因为属性代码写在最上面,所以它最先执行。这就打破了网上很多所谓 “属性一定先于方法” 的谣言。
  • 参数优先于方法:虽然 @ParamDec 贴在 saveData() 方法内部,但它的执行优先级高于包裹它的 @MethodDec。
  • 洋葱模型的嵌套:对于 saveData() 方法,TypeScript 先评估了外层的 OuterMethod,再评估了内层的 InnerMethod。但真正执行时,却是先执行 Inner,再执行 Outer。
  • 类装饰器永远压轴:无论类装饰器写在代码的哪个位置,它永远是最后被触发的 “大楼封顶” 操作。

在实际的企业级项目(如 NestJS)中,掌握装饰器的执行顺序,能够帮我们避开 90% 的逻辑陷阱:

  • 依赖注入(DI):参数装饰器先执行,是为了在方法运行前,确保所有需要的对象都已经 “标记” 好位置。
  • 权限校验(运行时的穿透陷阱):这也是大厂面试中最爱挖坑的地方。虽然在类加载时,装饰器函数的求值与执行是 “由下而上” 依次进行的;但在程序真正运行时,由于外层的装饰器包裹了内层的装饰器,真正的业务拦截顺序其实是 “由上而下(由外向内)” 穿透的。
给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号