一文读懂 @Decorator 装饰器——理解 VS Code 源码的基础

作者:easonruan,腾讯 CSIG 前端开发工程师

1. 装饰器的样子

我们先来看看 Decorator 装饰器长什么样子,大家可能没在项目中用过 Decorator 装饰器,但多多少少会看过下面装饰器的写法:

/* Nest.Js cats.controller.ts */
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

摘自《Nest.Js》官方文档

上述代码大家可以不着急去理解,主要是让大家对装饰器有一个初步了解,后面我们会逐一分析 Decorator 装饰器的实现原理以及具体用法。

2. 为什么要理解装饰器

2.1 浅一点来说,理解才能读懂 VS Code 源码

Decorator 装饰器是 ECMAScript 的语言提案,目前还处于 stage-2 阶段,但是借助 TypeScript 或者 Babel,已经有大量的优秀开源项目深度用上它了,比如:VS Code, Angular, Nest.Js(后端 Node.js 框架), TypeORM, Mobx(5) 等等

举个例子:https://github.com/microsoft/vscode/blob/main/src/vs/workbench/services/editor/browser/codeEditorService.ts#L22

作为一个有追求的程序员,你可能会问:上面代码的装饰器代表什么含义?去掉装饰器后能不能正常运行?

如果没弄懂装饰器,很难读懂 VS Code 这些优秀项目源码的核心思想。所以说你不需要熟练使用装饰器,但一定要理解装饰器的用法。

2.2 深一点来说,理解才能弄懂 AOP , IoC, DI 等优秀编程思想
1.AOP 即面向切面编程 (Aspect Oriented Programming)

AOP 主要意图是将日志记录,性能统计,安全控制,异常处理等代码从业务逻辑代码中划分出来,将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

简而言之,就是“优雅”的把“辅助功能逻辑”从“业务逻辑”中分离,解耦出来。

图摘自《简谈前端开发中的 AOP(一) -- 前端 AOP 的实现思路》

2.IoC 即 控制反转 (Inversion of Control),是解耦的一种设计理念
3.DI 即 依赖注入 (Dependency Injection),是 IoC 的一种具体实现

使用 IoC 前:

使用 IoC 后:

图摘自《两张图让你理解 IoC (控制反转)》

IoC 控制反转的设计模式可以大幅度地降低了程序的耦合性。而 Decorator 装饰器在 VS Code 的控制反转设计模式里,其主要作用是实现 DI 依赖注入的功能和精简部分重复的写法。由于该步骤实现较为复杂,我们先从简单的例子为切入点去了解装饰器的基本原理。

3. 装饰器的概念区分

在理解装饰器之前,有必要先对装饰器的 3 个概念进行区分。

3.1 Decorator Pattern (装饰器模式)

是一种抽象的设计理念,核心思想是在不修改原有代码情况下,对功能进行扩展。

3.2 Decorator (装饰器)

是一种特殊的装饰类函数,是一种对装饰器模式理念的具体实现。

3.3 @Decorator (装饰器语法)

是一种便捷的语法糖(写法),通过 @ 来引用,需要编译后才能运行。理解了概念之后可以知道:装饰器的存在就是希望实现装饰器模式的设计理念。

说法 1:在不修改原有代码情况下,对功能进行扩展。也就是对扩展开放,对修改关闭。

说法 2:优雅地把“辅助性功能逻辑”从“业务逻辑”中分离,解耦出来。(AOP 面向切面编程的设计理念)

4. 装饰器的实战:记录函数耗时

现在有一个 关羽(GuanYu) 类,它有两个函数方法:attack(攻击)run(奔跑)

class GuanYu {
  attack() {
    console.log('挥了一次大刀')
  }
  run() {
    console.log('跑了一段距离')
  }
}

而我们都是优秀的程序员,时时刻刻都有着经营思维 (性能优化),因此想给 关羽(GuanYu) 的函数方法提前做好准备:记录关羽的每一次 attack(攻击)run(奔跑) 的执行时间,以便于后期做性能优化。

4.1 做法一:复制粘贴,不用思考一把梭就是干

拿到需求,不用多想,立刻在函数前后,添加记录函数耗时的逻辑代码,并复制粘贴到其他地方:

class GuanYu {
  attack() {
+   const start = +new Date()
    console.log('挥了一次大刀')
+   const end = +new Date()
+   console.log(`耗时: ${end - start}ms`)
  }
  run() {
+   const start = +new Date()
    console.log('跑了一段距离')
+   const end = +new Date()
+   console.log(`耗时: ${end - start}ms`)
  }
}

但是这样直接修改原函数代码有以下几个问题:

  1. 理解成本高

统计耗时的相关代码与函数本身逻辑并无关系,对函数结构造成了破坏性的修改,影响到了对原函数本身的理解

  1. 维护成本高

如果后期还有更多类似的函数需要添加统计耗时的代码,在每个函数中都添加这样的代码非常低效,也大大提高了维护成本

4.2 做法二:装饰器模式,不修改原代码扩展功能
4.2.1 装饰器前置基础知识

在开始用装饰器实现之前必须掌握以下基础:

  1. Object.getOwnPropertyDescriptor()

返回指定对象上一个自有属性对应的属性描述符

var a = { b: () => {} }
var descriptor = Object.getOwnPropertyDescriptor(a, 'b')
console.log(descriptor)
/**
 * {
 *   configurable: true,  // 可配置的
 *   enumerable: true,    // 可枚举的
 *   value: () => {},     // 该属性对应的值(数值,对象,函数等)
 *   writable: true,      // 可写入的
 * }
 */

这里要注意一个点是:value 可以是 JavaScript 的任意值,比如函数方法,正则,日期等

  1. Object.defineProperty()

在一个对象上定义或修改一个属性的描述符:

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 'ThisIsNotWritable',
  writable: false
});

object1.property1 = 'newValue';
// throws an error in strict mode

console.log(object1.property1);
// expected output: 'ThisIsNotWritable'
4.2.2 【重点】手写一个装饰器函数

有了上面的两个基础后,我们开始利用装饰器模式的设计理念,用纯函数的形式写一个装饰器,实现记录函数耗时功能。为了让大家更深刻理解装饰器的原理,我们先不用 @Decorator 这个语法糖。

下面代码是本文的重点,大家可以放慢阅读速度,理解后再继续往下看:

// 装饰器函数
function decoratorLogTime(target, key) {
  const targetPrototype = target.prototype
  // Step1 备份原来类构造器上的属性描述符 Descriptor
  const oldDescriptor = Object.getOwnPropertyDescriptor(targetPrototype, key)

  // Step2 编写装饰器函数业务逻辑代码
  const logTime = function (...arg) {
    // Before 钩子
    let start = +new Date()
    try {
      // 执行原来函数
      return old
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值