21、观察者模式:JavaScript 中的事件管理利器

观察者模式: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

希望本文能帮助你更好地理解和应用观察者模式,在实际开发中发挥其优势,提高代码的质量和性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值