全新 Javascript 装饰器实战上篇:用 MobX 的方式打开 Vue

2023-10-20 11:42:06 浏览数 (3)

去年三月份装饰器提案进入了 Stage 3 阶段,而今年三月份 Typescript 在 5.0 也正式支持了 。装饰器提案距离正式的语言标准,只差临门一脚。

这也意味着旧版的装饰器(Stage 1) 将逐渐退出历史舞台。然而旧版的装饰器已经被广泛的使用,比如 MobX、Angular、NestJS… 未来较长的一段时间内,都会是新旧并存的局面。

本文将把装饰器语法带到 Vue Reactivity API 中,让我们可以像 MobX 一样,使用类来定义数据模型, 例如:

代码语言:javascript复制
class Counter {
  @observable
  count = 1

  @computed
  get double() {
    return this.count * 2
  }

  add = () => {
    this.count  
  }
}

在这个过程中,我们可以体会到新旧装饰器版本之间的差异和实践中的各种陷阱。

概览

关于装饰器的主要 API 都在上述思维导图中,除此之外,读者可以通过下文「扩展阅读」中提及的链接来深入了解它们。

Legacy

首先,我们使用旧的装饰器来实现相关的功能。

在 Typescript 下,需要通过 experimentalDecorators 来启用装饰器语法:

代码语言:javascript复制
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

如果使用 Babel 7 ,配置大概如下:

代码语言:javascript复制
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "legacy" }]
    ["@babel/plugin-transform-class-properties", {"loose": true }]
  ]
}

@observable

我们先来实现 @observable 装饰器,它只能作用于「类属性成员」,比如:

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

0 人点赞