JavaScript 自定义事件与观察者模式:原理、优缺点及代码实现

作者:像素猫时间:2026-10-25 13:58:04浏览:248

本文详解 JavaScript 中的观察者模式(发布-订阅模式),分析其在异步编程中替代回调函数的优势及潜在的性能与循环依赖风险。通过模拟 Node.js EventEmitter 和原生 DOM CustomEvent/CustomEvent API,展示如何构建松耦合的事件系统,帮助开发者理解并应用该设计模式。

一、前言

观察者模式,亦被称为发布-订阅模式或模型-视图模式,主要应用于存在一对多依赖关系的场景。其核心机制在于:当被观察对象(目标)的状态发生改变时,会自动通知所有依赖它的观察者对象。在软件设计模式中,观察者模式被归类为行为型模式。

为了直观理解观察者模式,可以将其类比为微信平台的公众号订阅机制。当用户订阅了某个公众号后,一旦该公众号发布新文章,系统会自动向所有订阅者推送通知。

作为订阅者,用户无需频繁手动检查公众号是否更新,发布者(公众号)会在合适的时间主动推送消息,实现了信息的自动分发。

这种机制使得订阅者与发布者之间解除了强耦合关系。发布者无需知晓订阅者的具体身份(如性别或是否为宠物),只需定时向所有注册的订阅者广播消息即可,实现了关注点分离。

二、观察者模式优缺点

优点:

  • 该模式广泛应用于异步编程场景,能够有效替代传统的回调函数,简化代码结构。
  • 开发者无需关注异步执行过程中的内部状态变化,只需监听事件完成的通知时间点即可,降低了逻辑复杂度。
  • 它取代了对象间硬编码的通知机制。对象之间通过事件进行松耦合交互,无需显式调用另一个对象的接口,提高了模块的独立性。
  • 通过抽象耦合关系降低了目标对象与观察者之间的耦合度,符合依赖倒置原则,便于系统的扩展和维护。

缺点:

  • 若被观察者对象拥有大量直接或间接的观察者,通知所有观察者可能会消耗较多时间,影响系统性能。
  • 如果观察目标与观察者之间存在循环依赖,可能会触发相互之间的无限循环调用,从而导致系统崩溃或栈溢出。
  • 观察者模式仅提供目标对象发生变化的通知,缺乏机制让观察者了解目标对象具体是如何发生变化的,信息粒度较粗。

三、代码实现

  • Event简易版
var Event = (function() {
  let list = {};
  let listen = function(key, fn) {
    (list[key] || (list[key] = [])).push(fn);
  };
  let remove = function(key, fn) {
    let fns = list[key];
    if (!fns) return;
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (let i = fns.length - 1; i >= 0; i--) {
        fns[i] === fn && (fns.splice(i, 1));
      }
    }
  };
  let trigger = function() {
    let keys = [].shift.call(arguments);
    let fns = list[keys];
    if (!fns || fns.length === 0) return;
    for (let i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments);
    }
  };
  return {
    listen,
    remove,
    trigger
  };
})();
// 使用
function log(name) {
  console.log(name)
}
Event.listen('to-do', log)    // 订阅事件
Event.trigger('to-do', '111') // 111
Event.trigger('to-do', '222') // 222
Event.remove('to-do', log)    // 移除事件
Event.trigger('to-do', '333') // 不执行
  • 类似Nodejs的EventEmitter模拟版
function EventEmitter() {
  this._maxListeners = 10;
  this._events = Object.create(null);
}
// 向事件队列添加事件
// prepend为true表示向事件队列头部添加事件
EventEmitter.prototype.addListener = function (type, listener, prepend) {
  if (!this._events) {
    this._events = Object.create(null);
  }
  if (this._events[type]) {
    if (prepend) {
      this._events[type].unshift(listener);
    } else {
      this._events[type].push(listener);
    }
  } else {
    this._events[type] = [listener];
  }
};
// 移除某个事件
EventEmitter.prototype.removeListener = function (type, listener) {
  if (Array.isArray(this._events[type])) {
    if (!listener) {
      delete this._events[type]
    } else {
      this._events[type] = this._events[type].filter(e => e !== listener && e.origin !== listener)
    }
  }
};
// 向事件队列添加事件,只执行一次
EventEmitter.prototype.once = function (type, listener) {
  const only = (...args) => {
    listener.apply(this, args);
    this.removeListener(type, listener);
  }
  only.origin = listener;
  this.addListener(type, only);
};
// 执行某类事件
EventEmitter.prototype.emit = function (type, ...args) {
  if (Array.isArray(this._events[type])) {
    this._events[type].forEach(fn => {
      fn.apply(this, args);
    });
  }
};
// 设置最大事件监听个数
EventEmitter.prototype.setMaxListeners = function (count) {
  this.maxListeners = count;
};
// 使用
var emitter = new EventEmitter();
var onceListener = function (args) {
  console.log('我只能被执行一次', args, this);
}
var listener = function (args) {
  console.log('我是一个listener', args, this);
}
emitter.once('click', onceListener);
emitter.addListener('click', listener);
emitter.emit('click', '参数');
emitter.emit('click');
emitter.removeListener('click', listener);
emitter.emit('click');

四、DOM自定义事件API

  • Event构造器
// 创建
var customEvent = new Event('custom-event');
// 监听
window.addEventListener('custom-event', function(e) {
  // do something ...
  console.log(`事件对象:`, e)
})
// 触发
window.dispatchEvent(customEvent)
  • CustomEvent构造器,可传递参数。
// 创建
var customEvent = new CustomEvent('custom-event', {
  data: {
    name: 'Tom',
    age: 18,
    other: 'otherMessage',
  }
});
// 监听
var eBox = document.getElementById('box')
eBox.addEventListener('custom-event', function(e) {
  // do something ...
  console.log(`传入的数据:`, e.data)
})
// 触发 兼容写法
if(window.dispatchEvent) {
  eBox.dispatchEvent(customEvent);
} else {
  // IE8
  eBox.fireEvent(customEvent);
}

以上就是JS中自定义事件与观察者模式详解的详细内容,更多关于JS自定义事件观察者模式的资料请关注本站其它相关文章!

作者最新文章