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

2021-08-27 11:10:29 浏览数 (1)

导语 | 本人在读VS Code源码的时候,发现其用了大量的@Decorator装饰器语法,由于对装饰器的语法比较陌生,它成为了我理解VS Code的拦路虎。其实不止VS Code,Angular、Node.js框架Nest.js、TypeORM、Mobx(5) 和Theia等都深度用到了装饰器语法,为了读懂各大优秀开源项目,让我们先一起来把@Decorator装饰器的原理以及用法彻底弄懂。

一、装饰器的样子

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

代码语言:javascript复制
/* 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》官方文档(网址:https://docs.nestjs.cn/8/controllers)

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

二、为什么要理解装饰器

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

Decorator装饰器是ECMAScript的语言提案,目前还处于stage-2阶段(https://github.com/tc39/proposal-decorators),但是借助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这些优秀项目源码的核心思想。所以说你不需要熟练使用装饰器,但一定要理解装饰器的用法。

(二)深一点来说,理解才能弄懂AOP,IoC,DI等优秀编程思想

  • AOP即面向切面编程 (Aspect Oriented Programming)

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

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

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

(https://zhuanlan.zhihu.com/p/269504590)

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

使用IoC前:

使用IoC后:

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

(https://learnku.com/laravel/t/3845/the-two-picture-lets-you-understand-ioc-inversion-of-control)

IoC控制反转的设计模式可以大幅度地降低了程序的耦合性。而Decorator装饰器在VS Code的控制反转设计模式里,其主要作用是实现DI依赖注入的功能和精简部分重复的写法。

由于该步骤实现较为复杂,我们先从简单的例子为切入点去了解装饰器的基本原理。

三、装饰器的概念区分

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

(一)Decorator Pattern(装饰器模式)

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

(二)Decorator(装饰器)

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

(三)@Decorator(装饰器语法)

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

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

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

四、装饰器的实战:记录函数耗时

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

代码语言:javascript复制
class GuanYu {  attack() {    console.log('挥了一次大刀')  }  run() {    console.log('跑了一段距离')  }}

而我们都是优秀的程序员,时时刻刻都有着经营思维(性能优化),因此想给关羽(GuanYu)的函数方法提前做好准备:

记录关羽的每一次attack(攻击)和run(奔跑)的执行时间,以便于后期做性能优化。

(一)复制粘贴,不用思考一把梭就是干

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

代码语言:javascript复制
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`)   }}

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

  • 理解成本高

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

  • 维护成本高

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

(二)装饰器模式,不修改原代码扩展功能

  • 装饰器前置基础知识

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

  • Object.getOwnPropertyDescriptor()(https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)

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

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

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

  • Object.defineProperty()(https://developer.mozilla.org/zhCN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)

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

代码语言:javascript复制
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'
  • 【重点】手写一个装饰器函数

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

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

代码语言:javascript复制
// 装饰器函数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 oldDescriptor.value.apply(this, arg) // 调用之前的函数    } finally {      // After 钩子      let end =  new Date()      console.log(`耗时: ${end - start}ms`)    }  }    // Step3 将装饰器覆盖原来的属性描述符的 value   Object.defineProperty(targetPrototype, key, {    ...oldDescriptor,    value: logTime        })}

class GuanYu {  attack() {    console.log('挥了一次大刀')  }  run() {    console.log('跑了一段距离')  }}// Step4 手动执行装饰器函数,装饰 GuanYu 的 attack 函数decoratorLogTime(GuanYu, 'attack')// Step4 手动执行装饰器函数,装饰 GuanYu 的 run 函数decoratorLogTime(GuanYu, 'run')

const guanYu = new GuanYu()guanYu.attack()// 挥了一次大刀// 耗时: 0msguanYu.run()// 跑了一段距离// 耗时: 0ms

以上就是装饰器的具体实现方法,其核心思路是:

  • Step1备份原来类构造器(Class.prototype) 的属性描述符(Descriptor)

利用Object.getOwnPropertyDescriptor获取

  • Step2 编写装饰器函数业务逻辑代码

利用执行原函数前后钩子,添加耗时统计逻辑

  • Step3 用装饰器函数覆盖原来属性描述符的value

利用Object.defineProperty代理

  • Step4 手动执行装饰器函数,装饰Class(类)指定属性

从而实现在不修改原代码的前提下,执行额外逻辑代码

 作者简介

阮易强(easonruan)

腾讯高级前端开发工程师

腾讯高级前端开发工程师,曾负责过「粤省事」、「穗康」等大型小程序项目,目前是WeDa微搭低代码平台(专有版)的核心开发人员,有丰富低代码平台研发经验。

 推荐阅读

go语言最全优化技巧总结,值得收藏!

如何用函数式编程思想优化业务代码,这就给你安排上!

拒绝代码臃肿,这套计算引擎设计方法值得一看!

保姆级教程: c 游戏服务器嵌入v8 js引擎


0 人点赞