装饰器目前还处于提案阶段,要在javascript中使用装饰器,我们必须借助
babel
或typescript
的转码能力
为什么要用装饰器
引入装饰器更能够便于代码逻辑的解藕和复用。举一个例子
举一个非常常见的需求。假设我们有一个类Network
,它有一个异步getList
方法
1 | class Network { |
有一天,我们想给它加个全局loading
,那么我们可能会这么写
1 | class Network { |
如果需要对另一个方法使用全局 loading,可能又需要再写一遍。并且这个代码还入侵了函数本身的逻辑。这时候使用装饰器就可以相对优雅解决这个问题。
实现一个loadingDecorator
装饰器
1 | function loadingDecorator(target, key, descriptor) { |
使用我们的装饰器
1 | class Network { |
这样,每当一个方法需要加 loading 的时候,给它使用@loadingDecorator
装饰器即可。这样即逻辑解藕又能实现比较好的代码复用
经过typescript
转码后的代码长这样,感兴趣的同学可以看看
1 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { |
什么是装饰器
装饰器是一种特殊的声明,可以作用在类的声明、方法、属性、访问器或者参数上。装饰器的用法是@decorator
。decorator
是一个函数,会在运行时的时候调用,对类进行一些修改。需要注意的是,在javascript
中,装饰器只能用于类,不能作用于普通函数。原因是函数会存在函数提升,设计者为了减少一些复杂性,可以参照一个讨论
如下就是定义一个装饰器函数,并且作用在类上
1 | function sealed(target) { |
其实就是类似于以下代码
1 | A = sealed(A); |
- 装饰器工厂
装饰器本质就是一个函数,所以也可以利用闭包的能力实现更多功能。装饰器工厂就是一个返回函数的函数,运行时将会被调用
1 | // 例如一个添加颜色的工厂装饰器 |
- 多个装饰器组合
装饰器也是支持多个一起使用的,还是上面 color 例子,添加多个 不同的color的装饰器
1 | "blue") ( |
从上面的信息,可以知道。
- 装饰器定义的执行顺序是从上到下
- 装饰器运行时装饰 class 的顺序是从下到上
装饰器的基本用法
类装饰器 (Class Decorators)
类装饰器作用于类的构造函数,可用于修改或者替换一个 class 定义
一个装饰器函数签名如下:
1 | type decorator = (target: Function) => Function | void; |
它接收被装饰的 class 作为target
函数的参数,如果装饰器函数有返回值,则使用这个返回值作为新的 class。
- 无返回值
1 | // 例如想直接修改一个class,给它新增一个静态方法 |
- 有返回值
当然,上面有返回值的形式直接返回也行。
1 | // 例如想继承被装饰的class |
类成员装饰器
下面列举的几个都是装饰到类的成员上,所以都可以归为一类
属性装饰器 (Property Decorators)
属性装饰器用于装饰属性,函数签名如下
1 | type decorator = ( |
属性装饰器的参数定义如下:
1、第一个参数。如果装饰的是静态方法,则是这个类 Target 本身;如果装饰的是原型方法,则是类的原型对象 Target.prototype
2、第二个参数。这个属性的名称
属性装饰器的返回值是被忽略的,所以如果需要修改属性值。分两种情况
- 静态属性,可以直接使用
Object.getOwnPropertyDescriptor(target, propertyKey)
和Object.defineProperty(target,propertyKey,{})
来获取和修改descriptor
- 如果是实例属性,则不能直接很方便的进行修改,因为 class 还没有进行实例化。何为定义实例属性,即如通过babel-plugin-proposal-class-properties直接语法定义的属性
1 | class Target { |
但这样的装饰器也不是没有作用,在 typescript 中可以很方便的收集元类型信息,后面的文章会说到
方法装饰器 (Method Decorators)
方法装饰器就是用来装饰方法,可以用来修改方法的定义。方法装饰器的函数签名如下
1 | type decorator = ( |
方法装饰器的参数定义如下:
1、第一个参数。如果装饰的是静态方法,则是这个类Target
本身;如果装饰的是原型方法,则是类的原型对象Target.prototype
2、第二个参数。这个方法的名称
3、第三个参数,这个方法的属性描述符,通过descriptor.value
可以直接拿到这个方法
如果属性装饰器有返回值,这个返回值讲作为这个方法的属性描述符。对象的属性描述符就是调用Reflect.getOwnPropertyDescriptor(target, propertyKey)
的返回值,详细可见
1 | const obj = { a: 1 }; |
1 | function log(target, key, descriptor) { |
- 静态/原型方法装饰器给方法添加 log
1 | // 静态或者动态方法添加log |
访问器装饰器 (Accessor Decorators)
参数装饰器 (Parameter Decorators)
参数装饰器的函数签名如下
1 | type decorator = ( |
参数装饰器的参数定义如下:
1、第一个参数。如果装饰的是静态方法的参数,则是这个类Target
本身;如果装饰的是原型方法的参数,则是类的原型对象Target.prototype
2、第二个参数。参数所处的函数名称
3、第三个参数,该参数位于函数参数列表的位置下标(number)
各种装饰器的执行顺序
如下:
1、先执行实例成员装饰器(非静态的),再执行静态成员装饰器
2、执行成员的装饰器时,先执行参数装饰器,再执行作用于成员的装饰器
3、执行完 1、2 后,执行构造函数的参数装饰器;最后执行作用于 class 的装饰器
typescript 更强大的装饰器
在vue-property-decorator
中的应用
上面提到的一些用法更多是 javascript 场景中使用装饰器优化我们代码的结构,在typescript
中,装饰器还有有一个更强大的功能,就是能在运行时去拿到我们在typescript
定义的时候类型信息。
如果用过typescript
写vue
的同学,一般会用到vue-decorator-property
这个库。在Prop
我们可以看到文档这样写
If you’d like to set type property of each prop value from its type definition, you can use
reflect-metadata
.
SetemitDecoratorMetadata
to true.
Import reflect-metadata before importing vue-property-decorator (importing reflect-metadata is needed just once.)
1 | import "reflect-metadata"; |
我们就不需要去在Prop
的options
的 type 再去定义一遍这个属性告诉 vue 了。这个能力正是typescript
的emitDecoratorMetadata
特性提供的。我们看上面的代码经过 ts 编译后的效果如下,地址
1 | import { __decorate, __metadata } from "tslib"; |
可见我们的类型信息被收集到 metadata 的design:type
中,通过reflect-metadata
提供的一些方法我们就能在运行时拿到这个类型信息。
可以理解为将每个被装饰的类/属性/方法的类型存放到一个全局的地方,key 为design:type
。后续处理的时候可以通过class
/method
/key
拿到这个类型信息,做一些我们想做的事情。
在 node 中的应用
来自深入理解 typescript的例子
如果我们想基于 class 声明编写 http 接口,而不是写很多router.get
/router.post
这样写法。例如如下:
1 | "/test") ( |
很显然,这里我们是定义了两个接口,分别是/test/a
和test/b
。这里的关键就在于实现Controller
和Post
/Get
装饰器
Controller
作用于 class 上,我们定义一个元信息key
并使用Reflect.defineMetadata
存对应的元信息
1 | const PATH_METADATA = Symbol('path'); |
再实现一个工厂装饰器,返回Get
/Post
1 | const PATH_METADATA = Symbol('path'); |
createMappingDecorator
接收一个参数(表示这是Get
还是Post
),返回一个装饰器。装饰器调用defineMetadata
存了PATH_METADATA
和METHOD_METADATA
两个 key,value 分别是请求路径和方法。
所以综上装饰后,可以类比一个以下形式的存储结构
1 | { |
取值并映射函数生成route
1 | // 取值 |
最后,只需把 route 相关信息绑在对应的http框架上即可
reflect-metadata
更多api可以参考
typedi
最后再简单介绍介绍typedi
引用文档的介绍。
typedi是一个 typescript(javascript)的依赖注入工具,可以在 node.js 和浏览器中构造易于测试和良好架构的应用程序。主要有以下特性:
- 基于属性/构造函数的依赖注入
- 单例/临时服务
- 可以支持多个
container
官网例子,非常方便实现依赖注入使用
1 | import { Container, Service } from 'typedi'; |
最后
码字不易,一键三连的人明年会有好运哦,祝大家新年快乐!!!
参考资料
- Post title:2020的最后一天,不妨了解下装饰器
- Post author:flytam
- Create time:2020-12-31 17:42:05
- Post link:https://blog.flytam.vip/2020的最后一天,不妨了解下装饰器.html
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.