深入理解JavaScript中的观察者模式:从原理到实战

作者:木棉

时间:2026-10-21

浏览:217

本文深入解析JavaScript中的观察者模式(发布/订阅模式),阐述其定义一对多关系的核心机制。通过浏览器事件、自定义事件及Redux等实例,说明如何利用中间者实现模块解耦。文章提供EmitterEvent类及createStore函数的代码实现,帮助开发者理解状态管理与事件驱动的开发优势。

文章目录

观察者模式简介

简介

观察者模式,亦称发布/订阅模式,定义了一种一对多的依赖关系。当主题对象状态发生改变时,会自动通知所有注册的观察者对象,使其能够自行更新。虽然该模式多用于底层代码开发,但引入它能使代码结构更加灵活、规整,有效减少冗余,并支持分模块、分功能的独立开发。

使用观察者模式的好处:

  1. 支持简单的广播通信,自动通知所有已订阅的对象。
  2. 页面载入后,目标对象与观察者之间可建立动态关联,从而提升系统的灵活性。
  3. 目标对象与观察者之间的抽象耦合关系,支持单独扩展及复用。

观察者模式的应用场景

引入

在前端业务开发中,应用较为广泛的场景是自定义事件。实际上,浏览器原生事件机制本身也是观察者模式的一种体现。


div.onclick = function click() {
 console.log('click')
}

在此示例中,函数订阅了div的click事件。当用户执行鼠标点击操作时,事件被发布,对应的函数随即执行。这里的函数即扮演了观察者的角色。

具象化理解与解耦优势

具象化理解

单纯从代码实现角度或许难以直观理解,但编程模式的设计往往源于生活经验,因此通过具象化的场景进行理解至关重要。

以举办婚礼为例。好友结婚并非日常琐事,一生可能仅发生一两次(甚至更多),因此“参加婚礼”这一行为也非每日进行。若天天询问“今天结婚吗,我来参加”,显然不合常理。若对方是单身人士,频繁如此询问可能引发反感。

此时,需要一个“事件发布”机制,即“通知”环节。

作为观察者,我订阅了“结婚”这一事件。基于好友关系,我承诺出席婚礼,这构成了订阅关系。此时,我就是观察者,“参加婚礼”即是对应的执行动作。一旦订阅完成,我不再需要每日询问,可专注于其他活动,如社交、娱乐等。

当“结婚”事件被发布并通知到我时,我便在特定时刻执行“参加婚礼酒席”这一行为函数。


//模拟代码
//我订阅了'marry' 事件
wo.on('marry',function(){
 //去参加婚礼酒席
})
//然后他发布。比如浏览器的点击
// 对应的我的 function就会执行

解耦/模块/功能

在代码实现中,需要一个类似中间服务的组件来管理发布与订阅过程。

例如浏览器中的事件处理程序,它提供了订阅接口,并接收“事件”信号发布给开发者。这使得JavaScript代码与浏览器之间建立了联系与互动,尽管二者本质不同。

观察者模式的最大优势在于解耦。它将原本耦合紧密的代码,按功能和模块拆分,使结构更清晰,降低开发成本,并提升可维护性。

具体案例:

在项目初期,View展示层与Model数据处理逻辑层往往耦合。例如,一个JS文件中可能同时包含接口请求、字符串拼接及DOM操作。这种写法导致单一函数既负责数据获取,又负责视图渲染。


var xhr = new XMLHttpRequest ()
 xhr.open('get',url)
 xhr.onreadystatechange = function () {
 if(this.readyState !== 4) return
 if(this.status === 200) {
 divs.innerHTML = '<p>' + this.response + '</p>'
 //
 }
 }
 xhr.responseType = 'json'
 xhr.send(null)

理想状态下,请求逻辑与视图渲染应当分离。


//请求
function getData () {
 var xhr = new XMLHttpRequest ()
 xhr.open('get',url)
 xhr.onreadystatechange = function () {
 if(this.readyState !== 4) return
 if(this.status === 200) {
 this.emit('渲染')
 // 发布
 }
 }
 xhr.responseType = 'json'
 xhr.send(null)
}
//渲染
function view () {}
xhr.on('渲染',view)

虽然在状态码为200时直接放置回调函数也能实现功能,但如果存在多个渲染函数且处理不同逻辑,每次均需修改函数名,且相同的请求过程需重复编写,效率低下。

采用观察者模式后:


function view1 () {}
function view2 () {}
function view3 () {}
function view4 () {}
if(我要渲染view1) {
 xhr.on('渲染',view1) //订阅
 xhr.on('渲染',view2)
}else{
 xhr.on('渲染',view3)
 xhr.on('渲染',view4)
}

函数仅负责数据请求,并暴露接口供其他方法添加监听。这样,成为一个完整的功能模块。无论后续有多少种渲染情况,内部的代码无需修改,实现了关注点分离。

在业务开发中,常因新增功能而修改已有代码,导致原有功能模块面目全非。

此外,这往往伴随着大量重复代码的产生。

过程? or 模块?

封装一个完整且良好的功能模块并非易事,但这是良好的开始。

订阅机制用于添加方法,发布事件时执行事件池中的回调。

框架中的观察者模式与实现

MV* 类框架

MVC是一种经典设计模式,其中也应用了观察者模式。

其内部通过大量的发布订阅机制,模拟内存中DOM的改变,并计算哪些DOM节点需要更新。当然,具体实现涉及诸多细节,此处不展开。

Redux

简单实现一个createstore函数。


//这是一个工厂函数,可以创建store
const createStore = (reducer) => {
 let state; // 定义存储的state
 let listeners = [];
 // getState的作用很简单就是返回当前是state
 const getState = ()=> state;
 //定义一个派发函数
 //当在外界调用此函数的时候,会修改状态
 const dispatch = (action)=>{
 //调用reducer函数修改状态,返回一新的状态并赋值给这个局部状态变量
 state = reducer(state,action);
 //依次调用监听函数,通知所有的监听函数
 listeners.forEach(listener => listener());
 }
 //订阅此状态的函数,当状态发生变化的时候记得调用此监听函数
 const subscribe = function(listener){
 //先把此监听 加到数组中
 listeners.push(listener);
 //返回一个函数,当调用它的时候将此监听函数从监听数组移除
 return function(){
  listeners = listeners.filter(l => l != listener);
 }
 }
 //默认调用一次dispatch给state赋一个初始值
 dispatch();
 return {
 getState,
 dispatch,
 subscribe
 }
}
let store = createStore(reducer);
//把数据渲染到界面上
const render = () => {
 document.body.innerText = store.getState();
}
// 订阅状态变化事件,当状态变化时用监听函数
store.subscribe(render);
render();
var INCREASE_ACTION = {type: 'INCREMENT'};
document.addEventListener('click', function (e) {
 //触发一个Action
 store.dispatch(INCREASE_ACTION);
})

在Node.js环境中,虽然较少直接使用EventEmitter,但多数对象会继承它。包括fs、net、http在内的支持事件响应的核心模块,均为EventEmitter的子类。

实现一个可以发布订阅的类。


'use strict'
class EmitterEvent {
 constructor() {
 //构造器。实例上创建一个事件池
 this._event = {}
 }
 //on 订阅
 on (eventName, handler) {
 // 根据eventName,事件池有对应的事件数组,
 就push添加,没有就新建一个。
 // 严谨一点应该判断handler的类型,是不是function
 if(this._event[eventName]) {
 this._event[eventName].push(handler)
 } else {
 this._event[eventName] = [handler]
 }
 }
 emit (eventName) {
 // 根据eventName找到对应数组
 var events = this._event[eventName];
 // 取一下传进来的参数,方便给执行的函数
 var otherArgs = Array.prototype.slice.call(arguments,1)
 var that = this
 if(events) {
 events.forEach((event) => {
 event.apply(that, otherArgs)
 })
 }
 }
 // 解除订阅
 off (eventName, handler) {
 var events = this._event[eventName]
 if(events) {
 this._event[eventName] = events.filter((event) => {
 return event !== handler
 })
 }
 }
 // 订阅以后,emit 发布执行一次后自动解除订阅
 once (eventName, handler) {
 var that = this
 function func () {
 var args = Array.prototype.slice.call(arguments,0)
 handler.apply(that, args)
 this.off(eventName,func)
 }
 this.on(eventName, func)
 }
}
var event = new EmitterEvent()
function a (something) {
 console.log(something,'aa-aa')
}
function b (something) {
 console.log(something)
}
 event.once('dosomething',a)
 event.emit('dosomething', 'chifan')
 //event.emit('dosomething')
// event.on('dosomething',a)
// event.on('dosomething',b)
// event.emit('dosomething','chifan')
// event.off('dosomething',a)
// setTimeout(() => {
// event.emit('dosomething','hejiu')
// },2000)

使用时,只需继承类。操作实例即可使用on、emit方法,实现发布订阅功能。例如XHR、组件开发等场景均可应用。

总结

以上就是本文的全部内容,希望有助于大家的学习或工作。如有疑问,欢迎留言交流。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章