深入理解JavaScript中的观察者模式:从原理到实战
本文深入解析JavaScript中的观察者模式(发布/订阅模式),阐述其定义一对多关系的核心机制。通过浏览器事件、自定义事件及Redux等实例,说明如何利用中间者实现模块解耦。文章提供EmitterEvent类及createStore函数的代码实现,帮助开发者理解状态管理与事件驱动的开发优势。
观察者模式简介
简介
观察者模式,亦称发布/订阅模式,定义了一种一对多的依赖关系。当主题对象状态发生改变时,会自动通知所有注册的观察者对象,使其能够自行更新。虽然该模式多用于底层代码开发,但引入它能使代码结构更加灵活、规整,有效减少冗余,并支持分模块、分功能的独立开发。
使用观察者模式的好处:
- 支持简单的广播通信,自动通知所有已订阅的对象。
- 页面载入后,目标对象与观察者之间可建立动态关联,从而提升系统的灵活性。
- 目标对象与观察者之间的抽象耦合关系,支持单独扩展及复用。
观察者模式的应用场景
引入
在前端业务开发中,应用较为广泛的场景是自定义事件。实际上,浏览器原生事件机制本身也是观察者模式的一种体现。
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、组件开发等场景均可应用。
总结
以上就是本文的全部内容,希望有助于大家的学习或工作。如有疑问,欢迎留言交流。
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01
