观察者模式:JavaScript 中的事件管理利器
1. 观察者模式简介
在事件驱动的环境中,如浏览器,用户的操作不断产生各种事件。观察者模式,也称为发布 - 订阅模式,是一种管理对象及其状态、动作之间关系的优秀工具。在 JavaScript 里,该模式能让我们观察程序中对象的状态变化,并在状态改变时收到通知。
在观察者模式中,有两个主要角色:观察者(订阅者)和被观察者(发布者)。下面我们通过报纸行业的例子来详细说明。
1.1 报纸投递示例
在报纸行业,有两个关键角色:读者(订阅者)和出版商(发布者)。
-
订阅者
:像你我这样的读者,我们接收报纸数据,阅读后做出反应,还能选择报纸投递的地址。当有新报纸数据(新闻)到达时,我们会收到通知,然后根据自己的喜好处理报纸,比如阅读后扔掉、分享给朋友或退回。
-
发布者
:负责生产报纸,如《旧金山纪事报》《纽约时报》和《萨克拉门托蜜蜂报》等。他们将报纸数据发送给订阅者,通常可以有多个订阅者,而一个订阅者也可以订阅多家报纸,这是一种多对多的关系。
1.2 推与拉模式
订阅者获取报纸有两种方式:推(Push)和拉(Pull)。
-
推模式
:出版商雇佣投递人员将报纸送到订阅者手中,主动推送数据。
-
拉模式
:小型地方出版物将报纸放在附近街角,让订阅者自行获取。这种方式适合资源有限的出版商,可优化投递效率。
2. JavaScript 中实现发布 - 订阅模式
2.1 角色与动作
在实现之前,我们先明确角色(对象)和动作(方法):
-
订阅者
:可以订阅和取消订阅,接收数据,有被投递或自行获取的选项。
-
发布者
:负责投递数据,有主动推送或被获取的选项。
2.2 示例代码
2.2.1 第一种实现方式
/*
* Publishers are in charge of "publishing" i.e. creating the event.
* They're also in charge of "notifying" (firing the event).
*/
var Publisher = new Observable;
/*
* Subscribers basically... "subscribe" (or listen).
* Once they've been "notified" their callback functions are invoked.
*/
var Subscriber = function(news) {
// news delivered directly to my front porch
};
Publisher.subscribeCustomer(Subscriber);
/*
* Deliver a paper:
* sends out the news to all subscribers.
*/
Publisher.deliver('extre, extre, read all about it');
/*
* That customer forgot to pay his bill.
*/
Publisher.unSubscribeCustomer(Subscriber);
在这个模式中,发布者主导,负责注册和取消订阅客户,并在有新报纸时进行投递。可观察对象有
subscribeCustomer
、
unSubscribeCustomer
和
deliver
三个方法,订阅方法接收订阅者函数作为回调,
deliver
方法通过回调将数据发送给订阅者。
2.2.2 第二种实现方式
/*
* Newspaper Vendors
* setup as new Publisher objects
*/
var NewYorkTimes = new Publisher;
var AustinHerald = new Publisher;
var SfChronicle = new Publisher;
/*
* People who like to read
* (Subscribers)
*
* Each subscriber is set up as a callback method.
* They all inherit from the Function prototype Object.
*/
var Joe = function(from) {
console.log('Delivery from '+from+' to Joe');
};
var Lindsay = function(from) {
console.log('Delivery from '+from+' to Lindsay');
};
var Quadaras = function(from) {
console.log('Delivery from '+from+' to Quadaras');
};
/*
* Here we allow them to subscribe to newspapers
* which are the Publisher objects.
* In this case Joe subscribes to the NY Times and
* the Chronicle. Lindsay subscribes to NY Times
* Austin Herald and Chronicle. And the Quadaras
* respectfully subscribe to the Herald and the Chronicle
*/
Joe.
subscribe(NewYorkTimes).
subscribe(SfChronicle);
Lindsay.
subscribe(AustinHerald).
subscribe(SfChronicle).
subscribe(NewYorkTimes);
Quadaras.
subscribe(AustinHerald).
subscribe(SfChronicle);
/*
* Then at any given time in our application, our publishers can send
* off data for the subscribers to consume and react to.
*/
NewYorkTimes.
deliver('Here is your paper! Direct from the Big apple');
AustinHerald.
deliver('News').
deliver('Reviews').
deliver('Coupons');
SfChronicle.
deliver('The weather is still chilly').
deliver('Hi Mom! I\'m writing a book');
在这个场景中,发布者作为
Publisher
对象,有
deliver
方法;订阅者作为回调函数,有
subscribe
和
unsubscribe
方法,这意味着我们扩展了
Function
原型来实现这些功能。订阅者拥有订阅和取消订阅的权力,发布者负责发送数据。
2.3 构建观察者 API
2.3.1 发布者构造函数
function Publisher() {
this.subscribers = [];
}
这个构造函数创建一个发布者对象,并初始化一个空的订阅者数组。
2.3.2 投递方法
Publisher.prototype.deliver = function(data) {
this.subscribers.forEach(
function(fn) {
fn(data);
}
);
return this;
};
deliver
方法使用
forEach
遍历订阅者数组,将数据传递给每个订阅者的回调函数。通过返回
this
,可以实现链式调用,一次传递多个数据。
2.3.3 订阅方法
Function.prototype.subscribe = function(publisher) {
var that = this;
var alreadyExists = publisher.subscribers.some(
function(el) {
if ( el === that ) {
return;
}
}
);
if ( !alreadyExists ) {
publisher.subscribers.push(this);
}
return this;
};
该方法扩展了
Function
原型,使所有函数都能调用
subscribe
方法。它先检查订阅者是否已存在,若不存在则将其添加到发布者的订阅者数组中,最后返回
this
以支持链式调用。
2.3.4 取消订阅方法
Function.prototype.unsubscribe = function(publisher) {
var that = this;
publisher.subscribers = publisher.subscribers.filter(
function(el) {
if ( el !== that ) {
return el;
}
}
);
return this;
};
unsubscribe
方法使用
filter
方法从发布者的订阅者数组中移除指定的订阅者。
2.3.5 一次性事件订阅示例
var publisherObject = new Publisher;
var observerObject = function(data) {
// process data
console.log(data);
// unsubscribe from this publisher
arguments.callee.unsubscribe(publisherObject);
};
observerObject.subscribe(publisherObject);
在这个示例中,
observerObject
订阅了
publisherObject
,在处理完数据后立即取消订阅。
3. 观察者模式在现实中的应用
3.1 动画中的应用
动画是实现可观察对象的一个很好的起点,有三个明显的可观察时刻:开始、结束和进行中。我们可以使用之前创建的
Publisher
工具来实现。
// Publisher API
var Animation = function(o) {
this.onStart = new Publisher,
this.onComplete = new Publisher,
this.onTween = new Publisher;
};
Animation.
method('fly', function() {
// begin animation
this.onStart.deliver();
for ( ... ) { // loop through frames
// deliver frame number
this.onTween.deliver(i);
}
// end animation
this.onComplete.deliver();
});
// setup an account with the animation manager
var Superman = new Animation({...config properties...});
// Begin implementing subscribers
var putOnCape = function(i) { };
var takeOffCape = function(i) { };
putOnCape.subscribe(Superman.onStart);
takeOffCape.subscribe(Superman.onComplete);
// fly can be called anywhere
Superman.fly();
// for instance:
addEvent(element, 'click', function() {
Superman.fly();
});
在这个例子中,
Animation
对象有三个发布者:
onStart
、
onComplete
和
onTween
。当动画开始、进行中或结束时,相应的发布者会发送通知给订阅者。
3.2 事件监听器也是观察者
在 DOM 脚本环境的高级事件模型中,事件监听器本质上就是内置的观察者。事件处理程序和事件监听器的区别在于:
-
事件处理程序
:将事件传递给一个指定的函数,且一个元素只能绑定一个回调函数。
-
事件监听器
:一个对象可以有多个监听器,每个监听器可以独立于其他监听器。
3.2.1 事件监听器示例
// example using listeners
var element = $('example');
var fn1 = function(e) {
// handle click
};
var fn2 = function(e) {
// do other stuff with click
};
addEvent(element, 'click', fn1);
addEvent(element, 'click', fn2);
在这个示例中,当元素被点击时,
fn1
和
fn2
都会被调用。
3.2.2 事件处理程序示例
// example using handlers
var element = document.getElementById('b');
var fn1 = function(e) {
// handle click
};
var fn2 = function(e) {
// do other stuff with click
};
element.onclick = fn1;
element.onclick = fn2;
在这个例子中,
fn1
会被
fn2
覆盖,
fn1
永远不会被调用。
3.3 何时使用观察者模式
观察者模式适用于需要将人类行为与应用程序行为抽象分离的场景。不建议用于直接处理与用户交互的基本 DOM 事件,如
click
、
mouseover
或
keypress
。例如,在导航系统中,当用户点击标签时,显示更多信息的菜单会切换。我们可以创建一个
onTabChange
可观察对象,让观察者在标签切换事件发生时得到通知,而不是直接监听
click
事件,这样可以避免代码与特定事件绑定,提高代码的灵活性。
3.4 观察者模式的优点
- 易于维护 :在大型架构中,能很好地维护基于动作的应用程序。
- 减少事件绑定 :通过一个事件监听器处理所有动作,并将信息分发给所有订阅者,减少内存占用,提高交互性能,避免为同一元素不断添加新的监听器。
3.5 观察者模式的缺点
设置可观察对象时会增加加载时间。可以使用懒加载技术来缓解这个问题,即延迟实例化新的可观察对象,直到需要时再创建,避免减慢应用程序的初始加载时间。
3.6 总结
观察者模式是一种抽象应用程序的好方法。可以广播事件,让开发者无需深入了解其他开发者的实现代码就能订阅事件。在浏览器这样的交互环境中,它非常理想。随着大型 Web 应用的发展,在代码中加入可观察对象能保持代码的可维护性和简洁性,避免第三方开发者或同事破坏代码。最后,我们开发的发布者工具使用的是“推”系统,你可以尝试编写一个“拉”系统,让订阅者主动从发布者那里获取数据。
以下是观察者模式的简单流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A([开始]):::startend --> B(发布者创建事件):::process
B --> C(订阅者订阅事件):::process
C --> D(发布者发布数据):::process
D --> E(订阅者接收数据并处理):::process
E --> F{是否继续}:::process
F -->|是| D
F -->|否| G([结束]):::startend
以下是观察者模式角色和方法的表格:
| 角色 | 方法 | 描述 |
| ---- | ---- | ---- |
| 发布者 |
deliver
| 发送数据给订阅者 |
| 发布者 |
subscribeCustomer
| 注册订阅者 |
| 发布者 |
unSubscribeCustomer
| 取消订阅者 |
| 订阅者 |
subscribe
| 订阅发布者的事件 |
| 订阅者 |
unsubscribe
| 取消订阅发布者的事件 |
希望通过本文,你对观察者模式有了更深入的理解,并能在实际开发中灵活运用。
4. 观察者模式的实际应用案例分析
4.1 电商系统中的购物车更新
在电商系统中,购物车的状态变化是一个典型的可使用观察者模式的场景。当用户添加商品到购物车、移除商品或者修改商品数量时,购物车的状态会发生改变,同时相关的组件(如总价显示、商品数量显示等)需要及时更新。
以下是一个简单的示例代码:
// 定义发布者(购物车)
function ShoppingCart() {
this.subscribers = [];
this.items = [];
}
// 投递方法,当购物车状态改变时通知订阅者
ShoppingCart.prototype.deliver = function() {
var totalPrice = 0;
var totalQuantity = 0;
this.items.forEach(function(item) {
totalPrice += item.price * item.quantity;
totalQuantity += item.quantity;
});
var data = {
totalPrice: totalPrice,
totalQuantity: totalQuantity
};
this.subscribers.forEach(function(fn) {
fn(data);
});
return this;
};
// 添加商品到购物车
ShoppingCart.prototype.addItem = function(item) {
this.items.push(item);
this.deliver();
};
// 移除商品
ShoppingCart.prototype.removeItem = function(index) {
this.items.splice(index, 1);
this.deliver();
};
// 订阅方法
Function.prototype.subscribe = function(publisher) {
var that = this;
var alreadyExists = publisher.subscribers.some(function(el) {
if (el === that) {
return true;
}
});
if (!alreadyExists) {
publisher.subscribers.push(this);
}
return this;
};
// 取消订阅方法
Function.prototype.unsubscribe = function(publisher) {
var that = this;
publisher.subscribers = publisher.subscribers.filter(function(el) {
if (el!== that) {
return el;
}
});
return this;
};
// 定义订阅者
var updateTotalPrice = function(data) {
console.log('Total Price: $' + data.totalPrice);
};
var updateTotalQuantity = function(data) {
console.log('Total Quantity: ' + data.totalQuantity);
};
// 创建购物车实例
var cart = new ShoppingCart();
// 订阅者订阅购物车事件
updateTotalPrice.subscribe(cart);
updateTotalQuantity.subscribe(cart);
// 添加商品到购物车
cart.addItem({ name: 'Book', price: 20, quantity: 1 });
cart.addItem({ name: 'Pen', price: 5, quantity: 2 });
// 移除一个商品
cart.removeItem(0);
在这个示例中,
ShoppingCart
是发布者,
updateTotalPrice
和
updateTotalQuantity
是订阅者。当购物车的商品发生变化时,调用
deliver
方法通知所有订阅者,订阅者根据接收到的数据更新相应的显示信息。
4.2 实时数据更新的仪表盘
在一些监控系统中,仪表盘需要实时显示各种数据的变化,如温度、湿度、电量等。当数据发生变化时,仪表盘的各个组件需要及时更新显示。
以下是一个简单的示例:
// 定义发布者(数据监控器)
function DataMonitor() {
this.subscribers = [];
this.data = {};
}
// 投递方法,当数据更新时通知订阅者
DataMonitor.prototype.deliver = function() {
this.subscribers.forEach(function(fn) {
fn(this.data);
}.bind(this));
return this;
};
// 更新数据
DataMonitor.prototype.updateData = function(newData) {
this.data = newData;
this.deliver();
};
// 订阅方法
Function.prototype.subscribe = function(publisher) {
var that = this;
var alreadyExists = publisher.subscribers.some(function(el) {
if (el === that) {
return true;
}
});
if (!alreadyExists) {
publisher.subscribers.push(this);
}
return this;
};
// 取消订阅方法
Function.prototype.unsubscribe = function(publisher) {
var that = this;
publisher.subscribers = publisher.subscribers.filter(function(el) {
if (el!== that) {
return el;
}
});
return this;
};
// 定义订阅者
var updateTemperature = function(data) {
console.log('Temperature: ' + data.temperature + '°C');
};
var updateHumidity = function(data) {
console.log('Humidity: ' + data.humidity + '%');
};
// 创建数据监控器实例
var monitor = new DataMonitor();
// 订阅者订阅数据更新事件
updateTemperature.subscribe(monitor);
updateHumidity.subscribe(monitor);
// 更新数据
monitor.updateData({ temperature: 25, humidity: 60 });
monitor.updateData({ temperature: 26, humidity: 62 });
在这个示例中,
DataMonitor
是发布者,
updateTemperature
和
updateHumidity
是订阅者。当数据更新时,调用
deliver
方法通知所有订阅者,订阅者根据接收到的数据更新相应的显示信息。
4.3 观察者模式在前端框架中的应用
许多前端框架都广泛使用了观察者模式,如 Vue.js 和 React。
Vue.js 中的响应式原理
Vue.js 通过观察者模式实现了数据的响应式。当数据发生变化时,Vue 会自动更新与之绑定的 DOM 元素。以下是一个简单的 Vue 示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue Example</title>
</head>
<body>
<div id="app">
<p>{{ message }}</p>
<button @click="changeMessage">Change Message</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello, Vue!'
},
methods: {
changeMessage: function() {
this.message = 'New Message!';
}
}
});
</script>
</body>
</html>
在这个示例中,
message
是一个可观察的数据,当
message
的值发生变化时,Vue 会自动更新页面上显示的内容。
React 中的状态管理
React 中的状态管理也可以使用观察者模式的思想。例如,使用
useState
和
useEffect
钩子可以实现类似的功能。以下是一个简单的 React 示例:
import React, { useState, useEffect } from'react';
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default App;
在这个示例中,
count
是一个状态变量,当
count
的值发生变化时,
useEffect
会被触发,更新页面的标题。
5. 总结与展望
5.1 总结
观察者模式是一种强大的设计模式,它在 JavaScript 中有着广泛的应用。通过将发布者和订阅者分离,实现了对象之间的解耦,提高了代码的可维护性和可扩展性。在大型架构中,观察者模式可以帮助我们更好地管理事件和状态变化,减少事件绑定,提高性能。
5.2 展望
随着 Web 应用的不断发展,观察者模式的应用场景也会越来越多。例如,在实时通信、物联网等领域,观察者模式可以用于处理实时数据的更新和分发。同时,随着前端框架的不断演进,观察者模式也会与其他设计模式和技术相结合,为开发者提供更强大的工具和更高效的开发方式。
在未来的开发中,我们可以进一步探索观察者模式的优化和扩展,如使用更高效的数据结构来存储订阅者,实现更复杂的事件过滤和分发机制等。同时,我们也可以将观察者模式与其他设计模式(如单例模式、工厂模式等)结合使用,构建更加健壮和灵活的应用程序。
以下是观察者模式在不同应用场景中的对比表格:
| 应用场景 | 发布者 | 订阅者 | 事件 |
| ---- | ---- | ---- | ---- |
| 报纸投递 | 出版商 | 读者 | 新报纸发布 |
| 购物车更新 | 购物车 | 总价显示、数量显示组件 | 商品添加、移除、数量修改 |
| 数据监控 | 数据监控器 | 温度显示、湿度显示组件 | 数据更新 |
| Vue.js | 数据对象 | DOM 元素 | 数据变化 |
| React | 状态变量 | 副作用函数 | 状态更新 |
以下是观察者模式在应用开发中的流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A([开始开发]):::startend --> B(定义发布者):::process
B --> C(定义订阅者):::process
C --> D(订阅者订阅发布者事件):::process
D --> E(发布者触发事件):::process
E --> F(发布者通知订阅者):::process
F --> G(订阅者处理事件):::process
G --> H{是否继续开发}:::process
H -->|是| E
H -->|否| I([结束开发]):::startend
希望本文能帮助你更好地理解和应用观察者模式,在实际开发中发挥其优势,提高代码的质量和性能。
超级会员免费看
921

被折叠的 条评论
为什么被折叠?



