去年三月份装饰器提案进入了 Stage 3 阶段,而今年三月份 Typescript 在 5.0 也正式支持了 。装饰器提案距离正式的语言标准,只差临门一脚。
这也意味着旧版的装饰器(Stage 1) 将逐渐退出历史舞台。然而旧版的装饰器已经被广泛的使用,比如 MobX、Angular、NestJS… 未来较长的一段时间内,都会是新旧并存的局面。
本文将把装饰器语法带到 Vue Reactivity API
中,让我们可以像 MobX 一样,使用类来定义数据模型, 例如:
class Counter {
@observable
count = 1
@computed
get double() {
return this.count * 2
}
add = () => {
this.count
}
}
在这个过程中,我们可以体会到新旧装饰器版本之间的差异和实践中的各种陷阱。
概览
关于装饰器的主要 API 都在上述思维导图中,除此之外,读者可以通过下文「扩展阅读」中提及的链接来深入了解它们。
Legacy
首先,我们使用旧的装饰器来实现相关的功能。
在 Typescript 下,需要通过 experimentalDecorators
来启用装饰器语法:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
如果使用 Babel 7 ,配置大概如下:
代码语言:javascript复制{
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "legacy" }]
["@babel/plugin-transform-class-properties", {"loose": true }]
]
}
@observable
我们先来实现 @observable
装饰器,它只能作用于「类属性成员
」,比如:
class Counter {
@observable
count = 1
}
const counter = new Counter()
expect(counter.count).toBe(1)
属性值可以是原始类型
或者对象类型
,没有限制。
为了让 Vue 的视图可以响应它的变化,我们可以使用 ref
来包装它。ref
刚好符合我们的需求,可以放置原始类型,也可以是对象, ref
会将其包装为 reactive
。
初步实现如下:
代码语言:javascript复制export const observable: PropertyDecorator = function (target, propertyKey) {
if (typeof target === 'function') {
throw new Error('Observable cannot be used on static properties')
}
if (arguments.length > 2 && arguments[2] != null) {
throw new Error('Observable cannot be used on methods')
}
const accessor: Initializer = (self) => {
const value = ref()
return {
get() {
return unref(value)
},
set(val) {
value.value = val
},
}
}
// 定义getter /setter 长远
Object.defineProperty(target, propertyKey, {
enumerable: true,
configurable: true,
get: function () {
// 惰性初始化
return initialIfNeed(this, propertyKey, accessor).get()
},
set: function (value) {
initialIfNeed(this, propertyKey, accessor).set(value)
},
})
}
解释一下上面的代码:
将装饰器的类型设置为 PropertyDecorator
。