Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >全新 Javascript 装饰器实战上篇:用 MobX 的方式打开 Vue

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

作者头像
_sx_
发布于 2023-10-20 03:42:06
发布于 2023-10-20 03:42:06
62400
代码可运行
举报
文章被收录于专栏:前端技术地图前端技术地图
运行总次数:0
代码可运行

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Counter {
  @observable
  count = 1

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

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

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

概览

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

Legacy

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "legacy" }]
    ["@babel/plugin-transform-class-properties", {"loose": true }]
  ]
}

@observable

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Counter {
  @observable
  count = 1
}

const counter = new Counter()
expect(counter.count).toBe(1)

属性值可以是原始类型或者对象类型,没有限制。

为了让 Vue 的视图可以响应它的变化,我们可以使用 ref 来包装它。ref 刚好符合我们的需求,可以放置原始类型,也可以是对象, ref 会将其包装为 reactive

初步实现如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

📢 对应的类型还有: ClassDecorator、MethodDecorator、ParameterDecorator ⚠️ 旧版装饰器使用位置上 Typescript 并没作类型检查,装饰器可以随意用在类、方法、属性各种位置上

可以通过 target 的类型,来判断装饰器作用于静态成员上还是实例成员上。如果是静态成员,target 是类本身;如果是实例成员,target 为类的原型对象(prototype)

属性装饰器只会接收两个参数:类和属性名。因为属性在构造函数中创建, 在类定义阶段,获取不到更多信息:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class A {
  foo = 1
}

// transpile to
class A {
  constructor() {
    this.foo = 1
  }
}

我们定义了一个新的 getter/setter 成员, 这样外部才能透明地使用 ref, 不需要加上 .value 后缀

惰性初始化 ref。旧版的装饰器并没有提供 addInitializer 这样的初始化钩子,我们曲线救国,使用惰性初始化的方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const REACTIVE_CACHE = Symbol('reactive_cache')
export interface ReactiveAccessor {
  get(): any
  set(value: any): void
}

function getReactiveCache(target: any): Record<string | symbol, any> {
  if (!hasProp(target, REACTIVE_CACHE)) {
    addHiddenProp(target, REACTIVE_CACHE, {})
  }

  return target[REACTIVE_CACHE]
}

export type Initializer = (target: any) => ReactiveAccessor

export function initialIfNeed(target: any, key: string | symbol, initializer: Initializer) {
  const cache = getReactiveCache(target)
  // 如果属性未定义,就执行初始化
  if (!hasProp(cache, key)) {
    cache[key] = initializer(target)
  }

  return cache[key]
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
这里我们将信息缓存在 REACTIVE_CACHE 字段中,实现惰性初始化。

写个单元测试看看:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
test('base type', () => {
  class A {
    @observable
    str = 'str'

    @observable
    num = 1

    @observable
    withoutInitialValue: any
  }

  const a = new A()

  let str
  let num
  let withoutInitialValue
  // 🔴 初始值应该正常被设置
  expect(a.str).toBe('str')
  expect(a.num).toBe(1)
  expect(a.withoutInitialValue).toBe(undefined)

  // 🔴 属性的变动应该被检测
  watchSyncEffect(() => {
    str = a.str
  })
  watchSyncEffect(() => {
    num = a.num
  })
  watchSyncEffect(() => {
    withoutInitialValue = a.withoutInitialValue
  })

  a.str = 'new str'
  a.num = 2
  a.withoutInitialValue = 'withoutInitialValue'

  expect(str).toBe('new str')
  expect(num).toBe(2)
  expect(withoutInitialValue).toBe('withoutInitialValue')
})

💥 在较新的构建工具中(比如 vite),上述的测试大概率无法通过!为什么?

经过调试会发现我们在 observable 中的 defineProperty 并没有生效?

通过阅读 Vite 的文档可以找到一些线索,即 Typescript 的 [useDefineForClassFields](https://cn.vitejs.dev/guide/features.html#usedefineforclassfields):

从 Vite v2.5.0 开始,如果 TypeScript 的 target 是 ESNextES2022 及更新版本,此选项默认值则为 true。这与 [tsc v4.3.2 及以后版本的行为](https://github.com/microsoft/TypeScript/pull/42663) 一致。这也是标准的 ECMAScript 的运行时行为

useDefineForClassFields 会改变类实例属性的定义方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class A {
  foo = 1
}

// 旧
class A {
  constructor() {
    this.foo = 1
  }
}

// 新:useDefineForClassFields
class A {
  constructor() {
    Object.defineProperty(this, 'foo', {
      enumerable: true,
      configurable: true,
      writable: true,
      value: 1,
    })
  }
}

这就是为什么我们装饰器内的 defineProperty 无法生效的原因。

解决办法:

方法 1: 显式关闭掉 useDefineForClassFields。如果是 Babel 需要配置 @babel/plugin-transform-class-propertiesloose 为 true:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "legacy" }]
    ["@babel/plugin-transform-class-properties", {"loose": true }]
  ]
}

方法 2: 或者模仿 MobX V6 的 API:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class TodoList {
  @observable todos = []

  @computed
  get unfinishedTodoCount() {
    return this.todos.filter((todo) => !todo.finished).length
  }

  constructor() {
    makeObservable(this)
  }
}

MobX 的 observable、computed 等装饰器只是收集了一些标记信息 本身不会对类进行转换,真正进行转换是在 makeObservable 中进行的, 而 makeObservable 的执行时机是在所有属性都初始化完毕之后。

由于本文只关注装饰器的能力,这里就不展开了,有兴趣的读者可以看下 MobX 的源码。

@computed

按照同样的方法,我们来实现一下 @computed 装饰器,MobX 的 computed 和 Vue 的 computed 概念基本一致,就是用来做衍生数据的计算。

@computed 只能应用在 getter 上面:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export const computed: MethodDecorator = function (target, propertyKey, descriptor) {
  // 不支持 static
  if (typeof target === 'function') {
    throw new Error('computed cannot be used on static member')
  }

  // 必须是 getter
  if (
    descriptor == null ||
    typeof descriptor !== 'object' ||
    typeof descriptor.get !== 'function'
  ) {
    throw new Error('computed can only be used on getter')
  }

  const initialGetter = descriptor.get
  const accessor: Initializer = (self) => {
    const value = vueComputed(() => initialGetter.call(self))

    return {
      get() {
        return unref(value)
      },
      set() {
        // readonly
      },
    }
  }

  descriptor.get = function () {
    // 惰性初始化
    return initialIfNeed(this, propertyKey, accessor).get()
  }
}
  • getter/setter/method 装饰器的用法一致。会接收 descriptor 作为第三个参数,我们可以对 descriptor 进行修改,或者返回一个新的 descriptor
  • 我们使用 vue 的 computed API 对 getter 函数进行简单包装。

测试一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
test('computed', () => {
  const count = ref(0)
  class A {
    @computed
    get double() {
      return count.value * 2
    }
  }

  const a = new A()
  let value
  watchSyncEffect(() => {
    value = a.double
  })

  expect(value).toBe(0)
  count.value++
  expect(value).toBe(2)
})

Ok, 没问题,可以正常运行。我们配合组件的实际场景再测试看看:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
test('render', () => {
  class A {
    @observable
    count = 1

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

  let count
  const a = new A()

  const Comp = defineComponent({
    setup() {
      watchSyncEffect(() => {
        count = a.double
      })

      return () => {
        /* ignore */
      }
    },
  })

  const { unmount } = render(Comp)

  let count2
  watchSyncEffect(() => {
    count2 = a.double
  })

  expect(count).toBe(2)
  expect(count2).toBe(2)

  a.count++
  expect(count).toBe(4)
  expect(count2).toBe(4)

  // 🔴 卸载
  unmount()

  a.count++
  expect(count).toBe(4)
  // 💥 received 4
  expect(count2).toBe(6)
})

上面的用例没有通过,在组件卸载之后,@computed 装饰的 double 就失去了响应性。Why?

解决这个问题之前,我们需要了解一下 [effectScope](https://cn.vuejs.org/api/reactivity-advanced.html#effectscope), effectScope 创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理和销毁。

Vue setup 就是包装在 effectScope 之下,如果我们的 computed 在 setup 下被初始化,就会被 setup 捕获,当组件卸载时就会被随之清理掉

我们的 @computed 是为全局作用域设计的,不能因为某个组件卸载而被销毁掉。为了解决这个问题,我们需要自己构造一个独立的 悬挂 effectScope (Detached effectScope ):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const accessor: Initializer = (self) => {
+   // true 标记为 detached
+   const scope = effectScope(true)
-   const value = vueComputed(() => initialGetter.call(self))
+   const value = scope.run(() => vueComputed(() => initialGetter.call(self)))

    return {
      get() {
        return unref(value)
      },
      set() {
        // readonly
      }
    }
  }

💡 watch 也会有相同的问题,读者可以自行尝试一下

💥 会不会内存泄露?理论上会泄露,取决于被 computed 订阅的数据源。如果该订阅源长期未释放,可能会出现内存泄露。

解决办法是将对应的类实例组件的生命周期绑定。当组件释放时,调用类实例的释放方法,例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const providerStore = <T,>(store: new () => T): T => {
  const instance = new store()
  // 将组件的 effectScope 传入实例中进行绑定
  instance.__effect_scope__ = getCurrentScope()
  return instance
}
// computed 实现调整
const scope = target.__effect_scope__ ?? effectScope(true)
// 在 setup 中调用
const store = providerStore(Store)

比如 全局Store 可以和 Vue App 绑定,页面 Store 可以和页面组件绑定。 🔴 MobX computed 并没有该问题,MobX 的 computed 在订阅者清空时,会「挂起(suspend)」,清空自己的订阅(除非显式设置了 keepAlive),从而可以规避这种内存泄露。详见这里 只能看后续 Vue 官方是否也作类似的支持了。


New

2022/3 装饰器议案正式进入 Stage 3 阶段,按照惯例,Typescript 也在 5.0 版本加入了该功能。

新版装饰器外形如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Decorator = (
  value: Input,
  context: {
    kind: string
    name: string | symbol
    access: {
      get?(): unknown
      set?(value: unknown): void
    }
    private?: boolean
    static?: boolean
    addInitializer?(initializer: () => void): void
  }
) => Output | void

相比旧版的装饰器,新版的 API 形式上更加统一了,并且提供了一些上下文信息,对于开发者来说更加便利。

核心的变化如下:

形式上更加统一,不管是什么位置,都遵循 (value, context) ⇒ output | void, 这个心智上更接近管道(pipe), 接收一个 Value , 可以返回一个新的 Value 来替换旧的 Value

linux 管道

context 提供了必要的上下文信息,对开发者来说更加便利,可以快速判断装饰器的类型、是否为静态属性、私有属性等等。

更倾向于将装饰器当做一个纯函数(管道、转换器)来使用,尽量不包含副作用(比如修改类的结构)。

为了限制副作用,装饰器基本上屏蔽了一些底层细节,比如 descriptor,构造函数、原型对象,这些在新的装饰器中基本拿不到。

副作用只能在 context.addInitializer 中调用,但是能力也非常有限。就拿属性装饰器来举例,initializer 通常在 class 内置的 defineProperty 之前调用,如果你在 initializer 中使用了 defineProperty,那么将被覆盖:

以 Typescript 的编译结果为例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Bar {
  @d
  foo = 1
}

// 编译结果:
let Bar = (() => {
    var _a;
    let _instanceExtraInitializers_1 = [];
    let _foo_decorators;
    let _foo_initializers = [];
    return _a = class Bar {
            constructor() {
                // 🔴 ③ 定义属性
                Object.defineProperty(this, "foo", {
                    enumerable: true,
                    configurable: true,
                    writable: true,
                    value:
                      // 🔴 ① 先执行其他装饰器的 addInitializer 回调
                      (__runInitializers(this, _instanceExtraInitializers_1),
                        // 🔴 ② 属性装饰器的 initializer
                        __runInitializers(this, _foo_initializers, 1))
                });
            }
        },
        (() => {
            _foo_decorators = [d];
            __esDecorate(null, null, _foo_decorators, { kind: "field", name: "foo", static: false, private: false, access: { has: obj => "foo" in obj, get: obj => obj.foo, set: (obj, value) => { obj.foo = value; } } }, _foo_initializers, _instanceExtraInitializers_1);
        })(),
        _a;

这样做的好处,笔者认为主要有以下几点:

  • 性能优化:旧版的装饰器可以对 class 进行魔改,这就导致了引擎在解析完 Class 体后再去执行装饰器时,最终的 Class 结构可能发生较大的改变,导致引擎的优化无法生效(来源:ECMAScript 双月报告:装饰器提案进入 Stage 3)。
  • 因为旧版可能会对类的结构进行破坏性魔改,这种副作用可能导致多个装饰器组合时,有难以预期的问题。
  • 更容易测试

另外 Typescript 针对新的装饰器也提供了更严格的类型检查,比如可以约束装饰器使用的位置,旧版可以使用在任意位置,只能通过运行时进行检查

Typescript 为新版装饰器提供了更严格的类型检查

💡 目前装饰器还未成为正式的语言特性,不排除后面还有特性变更。

💡 截止至文章发布的时间,Vite 使用新版装饰器还有一些问题。本文使用 Babel + Jest 来测试相关代码。

@observable

新版的属性装饰器 API 如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type ClassFieldDecorator = (
  value: undefined,
  context: {
    kind: 'field'
    name: string | symbol
    access: { get(): unknown; set(value: unknown): void }
    static: boolean
    private: boolean
  }
) => (initialValue: unknown) => unknown | void
  • value 始终为 undefined,因为属性在类定义时不存在,无法获取到初始值
  • context 没有 addInitializer 。属性装饰器的返回值是一个函数,这个实际上就是一个 initializer
  • 访问不到类和类的原型
  • 在 initializer 中也不能调用 defineProperty。原因见上文

也就是说,属性装饰器基本上堵死了我们去改造属性的机会


且慢,跟随装饰器发布的还有一个自动访问器(Auto Accessor)的特性(🙂 越来越像 Java、C# 了)

自动访问器使用 accessor 关键字定义:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class C {
  accessor x = 1
}

相当于:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class C {
  #x = 1

  get x() {
    return this.#x
  }

  set x(val) {
    this.#x = val
  }
}

这有啥用?稍安勿躁,它在装饰器场景有大用,先来看下它的 API:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type ClassAutoAccessorDecorator = (
  value: {
    get: () => unknown;
    set(value: unknown) => void;
  },
  context: {
    kind: "accessor";
    name: string | symbol;
    access: { get(): unknown, set(value: unknown): void };
    static: boolean;
    private: boolean;
    addInitializer(initializer: () => void): void;
  }
) => {
  get?: () => unknown;
  set?: (value: unknown) => void;
  init?: (initialValue: unknown) => unknown;
} | void;
  • value 接收 getter 和 setter
  • 可以返回新的 getter 和 setter
  • init 可以对初始值进行_转换_

它的妙用在于,我们可以「兵不血刃」(不改变结构或者新增属性)地实现拦截,看看我们 observable 的实现就知道了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function observable<This, Value>(
  value: ClassAccessorDecoratorTarget<This, Value>,
  context: ClassAccessorDecoratorContext<This, Value>
): ClassAccessorDecoratorResult<This, Value> | void {
  if (context.kind !== 'accessor') {
    throw new Error('observable can only be used on accessor')
  }

  if (context.static) {
    throw new Error('observable can not be used on static accessor')
  }

  return {
    init(val) {
      return ref(val)
    }
    get() {
      return (value.get.call(this) as Ref<Value>).value
    },
    set(val) {
      const ref = value.get.call(this) as Ref<Value>

      ref.value = val
    },
  }
}
  • 通过 context,我们可以更方便地判断是否是静态成员、是否装饰在预期的位置
  • 上述代码我们没有修改任何类的结构、新增任何属性。我们直接在 init 中将初始值转换为 ref, 相对应的 getter/setter 也作简单的改造。

很简单是不是?只不过,这个对已有的代码倾入性太大了,所有相关的属性都需要修改为 accessor, 但对于 API 使用者来说没什么区别:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class A {
  @observable
  accessor obj = {
    count: 1,
  }
}

@computed

Getter 装饰器和 Setter、Method 装饰器类型基本一致:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type ClassGetterDecorator = (
  value: Function,
  context: {
    kind: 'getter'
    name: string | symbol
    access: { get(): unknown }
    static: boolean
    private: boolean
    addInitializer(initializer: () => void): void
  }
) => Function | void

直接来看 computed 实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function computed<This, Return, Value extends () => Return>(
  value: Value,
  context: ClassGetterDecoratorContext<This, Return>
): Value | void {
  if (context.static) {
    throw new Error('computed cannot be used on static member')
  }

  if (context.kind !== 'getter') {
    throw new Error('computed can only be used on getter')
  }

  context.addInitializer(function (this: unknown) {
    const scope = effectScope(true)

    const val = scope.run(() => vueComputed(() => value.call(this)))

    Object.defineProperty(this, context.name, {
      configurable: true,
      enumerable: false,
      get() {
        return unref(val)
      },
    })
  })
}

通过 addInitializer 来添加初始化逻辑(副作用), this 为当前类的实例。旧版的装饰器并没有提供类似的时机,我们只能通过惰性初始化去模拟这种效果。

不过上面的程序也有个潜在的 BUG, 我们在新建一个 log 装饰器,组合在一起看看:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function log(value: Function, context: ClassGetterDecoratorContext) {
  return function (this: unknown) {
    console.log('start calling...')
    return value.apply(this)
  }
}

class A {
  @observable
  accessor count = 1

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

执行上述代码,我们会发现并没有打印 start calling... 邪恶的副作用…

主要原因是上述代码我们在 addInitializer 中引用的 ‘value’ 是类原始的 getter 值,而我们又重新用 defineProperty 覆盖了属性,导致 @log 装饰的值丢失了。

实际上在新版的装饰器中,更符合规范的用法是:返回新的值来替换旧的值

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const COMPUTED_CACHE: unique symbol = Symbol('computed_cache')

export function computed<This, Return, Value extends () => Return>(
  value: Value,
  context: ClassGetterDecoratorContext<This, Return>
): Value | void {
  // ...

  // 🔴 初始化缓存对象
  context.addInitializer(function (this: unknown) {
    if (!Object.prototype.hasOwnProperty.call(this, COMPUTED_CACHE)) {
      Object.defineProperty(this, COMPUTED_CACHE, {
        configurable: true,
        enumerable: false,
        value: new Map(),
      })
    }
  })

  return function (this: Object) {
    const cache = this[COMPUTED_CACHE] as Map<string | symbol, Ref<Return>>
    if (!cache.has(context.name)) {
      // 🔴 惰性初始化
      const scope = effectScope(true)

      const val = scope.run(() => vueComputed(() => value.call(this)))!

      cache.set(context.name, val)
    }

    return unref(cache.get(context.name))
  } as Value
}

上面的代码中,我们返回的新的函数来取代原有的 getter,另外在 addInitializer 中初始化缓存属性。我们建议在 addInitializer 中一次性将需要的属性都初始化完毕,避免在 getter 中动态去添加新的属性,利好 JavaScript 引擎的优化

这样做的好处是更符合新版装饰器的心智和设计意图,也可以保证装饰器按照组合的顺序调用。

总结

本文主要详细对比了新版和旧版的装饰器差异,通过实战将装饰器的能力和陷阱挖掘出来。

总得来说,新版的装饰器更加统一直观、更容易入手,在能力上也克制地收敛了。不过目前社区上大量的库和框架还停留在 Stage 1 装饰器,升级和改造需要较大的成本,我们可以暂时观望观望。

下一步:装饰器比较复杂的应用是依赖注入,当前的依赖注入库都深度依赖 reflect-metadata 来实现。而 Decorator Metadata 目前也进入了 Stage 3 阶段,很快就会和我们见面(Typescript 5.2),届时我们再聊聊如何实现依赖注入(🐶 看你们的点赞)。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-06-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
全新 JavaScript 装饰器实战下篇:实现依赖注入
上一篇文章我们介绍了 JavaScript 最新的装饰器提案,以及它和旧版的区别。这篇文章我们将继续深入装饰器,尝试实现一个简易的依赖注入库。
_sx_
2023/10/20
8120
全新 JavaScript 装饰器实战下篇:实现依赖注入
用故事解读 MobX源码(四) 装饰器 和 Enhancer
按照步骤,这篇文章应该写 观察值(Observable)的,不过在撰写的过程中发现,如果不先搞明白装饰器和 Enhancer(对这个单词陌生的,先不要着急,继续往下看) ,直接去解释观察值(Observable)会很费劲。因为在 MobX 中是使用装饰器设计模式实现观察值的,所以说要先掌握装饰器,才能进一步去理解观察值。
JSCON简时空
2020/03/31
9400
用故事解读 MobX源码(四) 装饰器 和 Enhancer
MobX 源码解析-observable
最近一直在用 MobX 开发中小型项目,开发起来真的,真的很爽,响应式更新,性能快,样板代码减少(相对 Redux)。所以,想趁 2019 年结束前把 MobX 源码研究一遍。
发声的沉默者
2021/06/14
7830
MobX 源码解析-observable
干货 | ES6 系列之我们来聊聊装饰器
       点击上方“腾讯NEXT学院”关注我们 Decorator 装饰器主要用于: 1. 装饰类 2. 装饰方法或属性 1 .装饰类 @annotationclass MyClass { } function annotation(target) { target.annotated = true;} 2. 装饰方法或属性 class MyClass { @readonly method() { }} function readonly(targe
腾讯NEXT学位
2020/02/11
6400
干货 | ES6 系列之我们来聊聊装饰器
MobX
也就是说,只要知道哪些东西是状态相关的(源于应用状态),在状态发生变化时,就应该自动完成状态相关的所有事情,自动更新UI,自动缓存数据,自动通知server
ayqy贾杰
2019/06/12
1.2K0
MobX
Javascript 装饰器极速指南
Decorators 是ES7中添加的JavaScript新特性。熟悉Typescript的同学应该更早的接触到这个特性,TypeScript早些时候已经支持Decorators的使用,而且提供了ES5的支持。本文会对Decorators做详细的讲解,相信你会体验到它给编程带来便利和优雅。 我在专职做前端开发之前, 是一名专业的.NET程序员,对.NET中的“特性”使用非常熟悉。在类、方法或者属性上写上一个中括号,中括号里面初始化一个特性,就会对类,方法或者属性的行为产生影响。这在AOP编程,以及ORM
用户1631416
2018/04/12
9580
Javascript 装饰器极速指南
javascript装饰器进入stage3了
在3月底,js的装饰器提案终于进入了stage3,同时其metadata部分单独拆开仍处于stage2阶段([详见](https://github.com/tc39/proposal-decorators/pull/454))。但是此装饰器却非平时我们广泛使用的装饰器。通过本文我们将了解下该js提案下装饰器的用法并对比和先前装饰器提案下用法的区别
ACK
2022/05/06
8060
javascript装饰器进入stage3了
【MobX】MobX 简单入门教程
<img src="http://images.pingan8787.com/blog/mobx.png" width="120px"/>
pingan8787
2019/10/24
1.6K0
一文读懂 JS 装饰器,这是一个会打扮的装饰器
装饰器是最新的 ECMA 中的一个提案,是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器在 Python 和 Java 等语言中也被大量使用。装饰器是实现 AOP(面向切面)编程的一种重要方式。
用户1462769
2020/03/30
1.4K0
一文读懂 JS 装饰器,这是一个会打扮的装饰器
ECMAScript 装饰器的 10 年
2015年,ECMAScript 6 发布,这是JavaScript语言的一个重大发布。这个版本引入了许多新特性,比如const/let、箭头函数、类等。大多数这些特性的目标是消除JavaScript的怪癖。因此,所有这些特性都被标记为“Harmony”。一些消息来源称整个ECMAScript 6被称为“ECMAScript Harmony”。除了这些特性,“Harmony”标签还突出了其他预计很快会成为规范一部分的特性。装饰器就是其中一种预期特性。
泯泷、
2024/03/16
1180
ECMAScript 装饰器的 10 年
Javascript装饰器的妙用
最近新开了一个Node项目,采用TypeScript来开发,在数据库及路由管理方面用了不少的装饰器,发觉这的确是一个好东西。 装饰器是一个还处于草案中的特性,目前木有直接支持该语法的环境,但是可以通过 babel 之类的进行转换为旧语法来实现效果,所以在TypeScript中,可以放心的使用@Decorator。
贾顺名
2019/12/09
1.1K0
一文读懂 @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()  
腾讯技术工程官方号
2021/08/09
1.2K0
TS 设计模式05 - 装饰者模式
在 oop 中,继承是实现多态最简单的方案。同一类的对象会有不同表现时,我们基于此基类去写派生类即可。但有时候,过度使用继承会导致程序无法维护。比如说,人有一个展示自己外观的方法,穿上不同的衣服这个展现形式就不一样。一个人可以选择穿 T-shirt,裤子,裙子,外套等等,它的顺序和搭配是不固定的,如果使用继承,我们对每种组合都需要去定义一个类,比如穿裤子的人,穿裙子的人,穿裤子和裙子的人,先穿裤子再穿外套的人......这样会是我们的程序变得非常庞大而难以维护。 事实上,不管穿什么衣服,本质上仍然是人,衣服只是基于人类的装饰而已。装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
love丁酥酥
2020/09/01
1.3K0
TS 设计模式05 - 装饰者模式
React MobX 开始
MobX[1] 用于状态管理,简单高效。本文将于 React 上介绍如何开始,包括了:
GoCoding
2021/12/30
1.1K0
React MobX 开始
Vue设计与实现读后感-响应式系统实现-场景增强computed与watch(三)- 2
我之前业务代码index.ts只是为了方便我在浏览器调试,并不能成为我代码健壮性的一部分。
吴文周
2022/03/30
1.6K0
设计模式(11)[JS版]-JavaScript中的注解之装饰器模式
装饰器模式模式动态地扩展了(装饰)一个对象的行为,同时又不改变其结构。在运行时添加新的行为的能力是由一个装饰器对象来完成的,它 "包裹 "了原始对象,用来提供额外的功能。多个装饰器可以添加或覆盖原始对象的功能。装饰器模式属于结构型模式。和适配器模式不同的是,适配器模式是原有的对象不能用了,而装饰器模式是原来的对象还能用,在不改变原有对象结构和功能的前提下,为对象添加新功能。
AlbertYang
2020/09/08
8980
设计模式(11)[JS版]-JavaScript中的注解之装饰器模式
React 进阶 - React Mobx
Mobx 采用了一种'观察者模式'—— Observer,整个设计架构都是围绕 Observer 展开:
Cellinlab
2023/05/17
9650
React 进阶 - React Mobx
浅谈JS中的装饰器模式
装饰器(Decorator)是ES7中的一个新语法,使用可参考阮一峰的文章。正如其字面意思而言,它可以对类、方法、属性进行修饰,从而进行一些相关功能定制。它的写法与Java的注解(Annotation)非常相似,但是功能还是有很大区别。
IMWeb前端团队
2019/12/03
1.3K0
浅谈JS中的装饰器模式
致敬Vue3: 1.1万字从零解读Vue3.0源码响应式系统
原文地址:https://hkc452.github.io/slamdunk-the-vue3/
胡哥有话说
2020/09/29
9140
《现代Javascript高级教程》装饰器
在JavaScript中,修饰器(Decorator)是一种特殊的语法,用于修改类、方法或属性的行为。修饰器提供了一种简洁而灵活的方式来扩展和定制代码功能。本文将详细介绍JavaScript修饰器的概念、语法和应用场景,并提供相关的代码示例。
linwu
2023/07/27
6210
推荐阅读
相关推荐
全新 JavaScript 装饰器实战下篇:实现依赖注入
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验