Skip to content

封装时更好的写法

现在有这样一个类,提供了事件对象的实现:

ts
export class EventCenter {
  public on(eventName: Event, handler) {
    // ...
  }

  public once(eventName: Event, handler) {
    handler.once = 1 // 标志只触发一次
    this.on(eventName, handler)
  }

  public emit(eventName: Event, ...options) {
    // ...
    handler.call(this, ...options) // 触发handler函数

    // 如果是一次性函数 -> 卸载
    if (handler?.once === 1) {
      this.off(eventName, handler)
    }
    // ...
  }

  public off(eventName?, handler?) {
    // ...
  }
}

其中 once 函数提供了在调用一次后即刻销毁的事件挂载方式。

接下来我们看看如何更好的实现,优化后的代码:

ts
export class EventCenter {
  public on(eventName: Event, handler) {
    // ...
  }

  public once(eventName: Event, handler) {
    const wrappedHandler = (...options) => {
      handler.call(this, ...options)
      this.off(eventName, wrappedHandler)
    }

    wrappedHandler.once = true
    this.on(eventName, wrappedHandler)
  }

  public emit(eventName: Event, ...options) {
    // ...
    handler.call(this, ...options)
    // ...
  }

  public off(eventName?, handler?) {
    // ...
  }
}

提示

之所以这样修改,在给函数对象挂载属性时,一般来说,最好 避免修改函数自身的属性 。这是因为 函数应该专注于执行特定的功能,而不应该被用于存储状态或附加其他行为

在修改前的代码中给 handler 函数添加了一个 once 属性来表示执行一次后自动销毁。尽管这样的做法可以实现一次性的事件订阅,但会破坏函数的单一职责原则并增加复杂性

如何理解这里的函数的单一职责原则?

在修改前的代码中,我们为了实现 once 这一功能,需要 onceemit 函数共同发挥作用。使 emit 函数不再纯粹(掺杂了 once 所需要的 off 调用),这显然不是一个好的设计。

在修改后的代码里, once 功能完全由 once 函数完成🎉。

推荐的更好写法是使用闭包来管理状态,以实现一次性的事件订阅。可以通过创建一个包装函数,在内部维护一个标志位来控制回调函数的执行次数。

在修改后的代码中,once 方法会创建一个新的包装函数 wrappedHandler 来处理事件回调。这个包装函数会在执行后自动解绑事件。通过使用闭包和额外的标志位,我们避免了直接修改原始函数的属性,使代码更清晰、易于理解,并且保持了函数的单一职责。

总结起来,使用闭包来管理状态和附加行为会比直接在函数上挂载属性更好,因为它遵循了良好的设计原则,并提供了更可控和可维护的代码。