跳至主要內容

参与者模式

微信公众号:储凡2023/11/29大约 10 分钟

参与者模式

JavaScript中的参与者模式,就是在特定的作用域中执行给定的函数,并将参数原封不动的传递, 参与者模式不属于一般定义的23种设计模式的范畴,而通常将其看作广义上的技巧型设计模式。

基本介绍

参与者模式实现的在特定的作用域中执行给定的函数,并将参数原封不动的传递,实质上包括函数绑定和函数柯里化。

对于函数绑定,它将函数以函数指针(函数名)的形式传递,使函数在被绑定的对象作用域中执行, 因此函数的执行中可以顺利地访问到对象内部的数据,由于函数绑定构造复杂,执行时需要消耗更多的内存, 因此执行速度上要稍慢一些,不过相对于解决的问题来说这种消耗还是值得的, 因此它常用于事件,setTimeoutsetInterval等异步逻辑中的回调函数。

示例

当我们需要为元素添加事件时,可以封装一个兼容的方法。

const A = { event: {} }

A.event.on = function (dom, type, fn) {
  if (dom.addEventListener) {
    dom.addEventListener(type, fn, false)
  }
  else if (dom.attachEvent) {
    dom.attachEvent(`on${type}`, fn)
  }
  else {
    dom[`on${type}`] = fn
  }
}

这样对于事件绑定是完全可以的,但是我们不能传入一些参数以保持回调函数内部的词法作用域的数据的完整性,所以我们在回调函数中下点文章。

A.event.on = function (dom, type, fn, data) {
  if (dom.addEventListener) {
    dom.addEventListener(type, (e) => {
      fn.call(dom, e, data)
    })
  }
}

这样又导致了新问题,添加的事件回调函数不能移除了,所以还需要对其改进,使用bind改变绑定函数的this ,实际上就是形成了一个闭包,现在bind普遍都在Function.prototype 上实现了,但是在一些低版本浏览器还是需要自行实现,我们自行实现一个bind

function bind(fn, context) {
  return function () {
    return fn.apply(context, arguments)
  }
}

尝试使用bind绑定this,接下来我们在事件绑定中就可以传递使用bind绑定的函数了。

const obj = {
  title: 'test',
}

function test() {
  console.log(this.title)
}

const bindTest = bind(test, obj)

bindTest() // test

我们可以继续完善bindbind实际上是可以保存部分参数的,这时我们可以借助函数柯里化的思想将参数分开实现,当然直接使用ES6...操作符也是可以直接实现的。柯里化Currying是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术,是函数式编程应用。

// 柯里化可以参考 https://blog.touchczy.top/#/JavaScript/Js%E4%B8%ADCurrying%E7%9A%84%E5%BA%94%E7%94%A8
function bind(fn, context) {
  const slice = Array.prototype.slice
  const args = slice.call(arguments, 2)
  return function () {
    const addArgs = slice.call(arguments)
    const allArgs = addArgs.concat(args)
    return fn.apply(context, allArgs)
  }
}

const obj = {
  title: 'test',
}

function test(name) {
  console.log(this.title, name)
}

const bindTest = bind(test, obj, 1)

bindTest() // test 1
function bind(fn, context, ...args) {
  return function () {
    return fn.apply(context, args)
  }
}

const obj = {
  title: 'test',
}

function test(name) {
  console.log(this.title, name)
}

const bindTest = bind(test, obj, 1)

bindTest() // test 1

实际上不光在事件绑定的时候我们使用到bind绑定作用域,在使用观察者模式时订阅的过程通常也是传递了一个绑定好的函数。

function PubSub() {
  this.handlers = {}
}

PubSub.prototype = {
  constructor: PubSub,
  on(key, handler) { // 订阅
    if (!(key in this.handlers))
      this.handlers[key] = []
    if (!this.handlers[key].includes(handler)) {
      this.handlers[key].push(handler)
      return true
    }
    return false
  },

  once(key, handler) { // 一次性订阅
    if (!(key in this.handlers))
      this.handlers[key] = []
    if (this.handlers[key].includes(handler))
      return false
    const onceHandler = (args) => {
      handler.apply(this, args)
      this.off(key, onceHandler)
    }
    this.handlers[key].push(onceHandler)
    return true
  },

  off(key, handler) { // 卸载
    const index = this.handlers[key].findIndex(item => item === handler)
    if (index < 0)
      return false
    if (this.handlers[key].length === 1)
      delete this.handlers[key]
    else this.handlers[key].splice(index, 1)
    return true
  },

  commit(key, ...args) { // 触发
    if (!this.handlers[key])
      return false
    console.log(key, 'Execute')
    this.handlers[key].forEach(handler => handler.apply(this, args))
    return true
  },

}

const obj = {
  name: 'me',
  say() {
    console.log(this.name)
  }
}

const eventBus = new PubSub()

eventBus.on('say', obj.say)
eventBus.on('say', obj.say.bind(obj))

eventBus.commit('say')

// say Execute // 这是触发这个事件的log
// undefined   // 未绑定this的触发
// me          // 绑定this的触发

更新日志

2024/7/29 16:44
查看所有更新日志
  • a3cca-refactor: 替换eslint规则,使用antfu/eslint模块 (#138)
  • c0f2d-refactor: 升级vuepress相关版本,优化项目结构 (#137)
  • 6849e-feat(ORM): 新增Sequelize教程文档、代码
  • e6a79-feat: 大幅新增设计模式、Linux命令、ORM框架教程文档