导航
转载自:https://segmentfault.com/a/1190000038957410
观察者模式的定义是在对象之间定义一个一对多的依赖,当对象自身状态改变的时候,会自动通知给关心该状态的观察者。
解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。
这种对象与对象,有点像 商家-顾客 的关系,顾客对商家的某个商品感兴趣,就被商家记住,等有新品发布,便会直接通知顾客,相信加过微商微信会深有体会。
来张图直观感受:
可以从图中看出来,这种模式是商家直接管理顾客。
该模式理解起来和观察者模式一样,也是定义一对多的依赖关系,对象状态改变后,通知给所有关心这个状态的订阅者。
订阅发布模式有订阅的动作,可以不和商家直接产生联系,只要能订阅上关心的状态即可,通常利用第三方媒介来做,而发布者也会利用三方媒介来通知订阅者。
这有点像 商家-APP-顾客 的关系,某个产品断货,顾客可以在APP上订阅上货通知,待上新,商家通过APP通知订阅的顾客。
在程序实现中,第三方媒介称之为 EventBus(事件总线/事件调度中心),可以理解为订阅事件的集合,它提供订阅、发布、取消等功能。订阅者订阅事件,和发布者发布事件,都通过事件总线进行交互。
从概念上理解,两者没什么不同,都在解决对象之间解耦,通过事件的方式在某个时间点进行触发,监听这个事件的订阅者可以进行相应的操作。
在实现上有所不同,观察者模式中的发布者自身来维护订阅者,后续的一些列操作都要通过发布者完成;订阅发布模式是订阅者和发布者中间会有一个事件总线,操作都要经过事件总线完成。
观察者模式的事件名称,通常由发布者指定发布的事件,当然也可以自定义,这样看是否提供自定义的功能。
在 DOM 中绑定事件,click、mouseover 这些,都是内置规定好的事件名称。
document.addEventListener('click',()=>{})
addEventListener 第一个参数就是绑定的时间名称;第二参数是一个函数,就是订阅者。
订阅发布模式的事件名称就比较随意,在事件总线中会维护一个事件对应的订阅者列表,当该事件触发时,会遍历列表通知所有的订阅者。
伪代码:
// 订阅
EventBus.on('custom', () => {})
// 发布
EventBus.emit('custom')
事件名称为开发者自定义,当使用频繁时维护起来较为麻烦,尤其是改名字,多个对象或组件都要替换,通常会把事件名称在一个配置中统一管理。
在 Javascript 中函数就是对象,订阅者对象可以直接由函数来充当,就跟绑定 DOM 使用的 addEventListener 方法,第二个参数就是订阅者,是一个函数。
我们从上面描述的概念中去实现 商家-顾客,这样可以更好的理解(或者迷糊)。
定义一个顾客类,需要有个方法,这个方法用来接收商家通知的消息,就跟顾客都留有手机号码一样,发布的消息都由手机来接收,顾客收消息的方式是统一的。
// 顾客
class Customer {
update(data) {
console.log(`拿到了数据: ${data}`);
}
}
定义商家,商家提供订阅、取消订阅、发布功能
// 商家
class Merchant {
constructor() {
this.handlers = {};
}
addListener(type, customer) {
if (!this.handlers[type]) {
this.handlers[type] = [];
}
if (!this.handlers[type].includes(customer)) {
this.handlers[type].push(customer);
}
}
removeListener(type, customer) {
if (this.handlers[type].length > 0) {
this.handlers[type] = this.handlers[type].filter(c => c !== customer);
}
}
notifyListener(type, data) {
if (this.handlers[type].length > 0) {
this.handlers[type].forEach(c => {
c.update(data);
});
}
}
}
使用一下:
// 多名顾客
const c1 = new Customer()
const c2 = new Customer()
const c3 = new Customer()
// 商家
const m = new Merchant()
// 顾客订阅商家商品
m.addListener('shoes', c1)
m.addListener('shoes', c2)
m.addListener('skirt', c3)
// 过了一天没来,取消订阅
setTimeout(() => {
m.removeListener('shoes', c2)
}, 1000)
// 过了几天
setTimeout(() => {
m.notifyListener('shoes', '来啊,购买啊')
m.notifyListener('skirt', '降价了')
}, 2000)
订阅和发布的功能都在事件总线中。
class EventEmitter {
handlers = {
// 事件类型type: [订阅者1, 订阅者2, ...]
// e.g. : click: [handler1, handler2, handler3]
}
// type: click事件/change事件...
on(type, handler, once = false) {
if (!this.handlers[type]) {
this.handlers[type] = [];
}
if (!this.handlers[type].includes(handler)) {
this.handlers[type].push(handler);
handler.once = once;
}
}
once(type, handler) {
this.on(type, handler, true);
}
off(type, handler) {
if (this.handlers[type]) {
this.handlers[type] = this.handlers[type].filter(h => {
return h !== handler;
})
}
}
trigger(type, params) {
if (this.handlers[type]) {
this.handlers[type].forEach(handler => {
handler.call(this, params);
if (handler.once) {
this.off(type, handler);
}
});
}
}
}
使用:
const ev = new EventEmitter();
function handler1(params) {
console.log("handler1", params);
}
function handler2(params) {
console.log("handler2", params);
}
function handler3(params) {
console.log("handler3", params);
}
ev.on("test", handler1);
ev.on("test", handler2);
ev.on("test", handler3);
ev.trigger("test", 123);
通过以上两种模式的实现上来看,观察者模式进一步抽象,能抽出公共代码就是事件总线,反过来说,如果一个对象要有观察者模式的功能,只需要继承事件总线。
node 中提供能了 events 模块可供我们灵活使用。
继承使用,都通过发布者调用:
const EventEmitter = require('events')
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter()
myEmitter.on('event', (data) => {
console.log('触发事件', data);
});
myEmitter.emit('event', 1);
直接使用,当做事件总线:
const EventEmitter = require('events')
const emitter = new EventEmitter()
emitter.on('custom', (data) => {
console.log('接收数据', data);
})
emitter.emit('custom', 2)
观察者模式在很多场景中都在使用,除了上述中在 DOM 上监听事件外,还有最常用的是 Vue 组件中父子之间的通信。
父级代码:
<template>
<div>
<h2>父级</h2>
<Child @custom="customHandler"></Child>
</div>
</template>
<script>
export default {
methods: {
customHandler(data){
console.log('拿到数据,我要干点事', data);
}
}
}
</script>
子级代码:
<template>
<div>
<h2>子级</h2>
<button @click="clickHandler">改变了</button>
</div>
</template>
<script>
export default {
methods: {
clickHandler(){
this.$emit('custom', 123)
}
}
}
</script>
子组件是一个通用的组件,内部不做业务逻辑处理,仅仅在点击时会发布一个自定义的事件 custom。子组件被使用在页面的任意地方,在不同的使用场景里,当点击按钮后子组件所在的场景会做相应的业务处理。如果关心子组件内部按钮点击这个状态的改变,只需要监听 custom 自定义事件。
订阅发布模式在用 Vue 写业务也会使用到,应用场景是在跨多层组件通信时,如果利用父子组件通信一层层订阅发布,可维护性和灵活性很差,一旦中间某个环节出问题,整个传播链路就会瘫痪。这时采用独立出来的 EventBus 解决这类问题,只要能访问到 EventBus 对象,便可通过该对象订阅和发布事件。
// EventBus.js
import Vue from 'vue'
export default const EventBus = new Vue()
父级代码:
<template>
<div>
<h2>父级</h2>
<Child></Child>
</div>
</template>
<script>
import EventBus from './EventBus'
export default {
// 加载完就要监控
moutend(){
EventBus.on('custom', (data) => {
console.log('拿到数据', data);
})
}
}
</script>
<template>
<div>
<h2>嵌套很深的子级</h2>
<button @click="clickHandler">改变了</button>
</div>
</template>
<script>
import EventBus from './EventBus'
export default {
methods: {
clickHandler(){
EventBus.emit('custom', 123)
}
}
}
</script>
通过上述代码可以看出来订阅发布模式完全解耦两个组件,互相可以不知道对方的存在,只需要在恰当的时机订阅或发布自定义事件。****
Vue2 中会通过拦截数据的获取进行依赖收集,收集的是一个个 Watcher。等待对数据进行变更时,要通知依赖的 Watcher 进行组件更新。可以通过一张图看到这个收集和通知过程。
这些依赖存在了定义的 Dep 中,在这个类中实现了简单的订阅和发布功能,可以看做是一个 EventBus,源码如下:
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
每个 Wather 就是订阅者,这些订阅者都实现一个叫做 update 的方法,当数据更改时便会遍历所有的 Wather 调用 update 方法。
通过上述的表述,相信你对观察者模式和订阅发布模式有了重新的认识,可以说二者是相同的,它们的概念和解决的问题是一样的,致力于让两个对象解耦,只是叫法不一样;也可以说二者不一样,在使用方式和场景中不一样。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="list"></ul>
<script src="./Observer.js"></script>
<script src="./index.js"></script>
</body>
</html>
Observer.js
class Target {
constructor(data) {
this.data = data;
this.observer = new Observer('#list');
this.init();
}
init() {
this.validateData(this.data);
this.proxyData();
}
validateData(data) {
const { username, password, age, gender } = data;
username.length < 6 && (data.username = '');
password.length < 6 && (data.password = '');
typeof age !== 'number' && (data.age = 0);
!['male', 'female'].includes(gender) && (data.gender = 'male');
}
proxyData() {
const _this = this;
for (let key in this.data) {
Object.defineProperty(this, key, {
get() {
this.observer.updateLog('get', key, _this.data[key]);
return _this.data[key];
},
set(newValue) {
this.observer.updateLog('set', key, _this.data[key], newValue);
_this.data[key] = newValue;
}
})
}
}
}
// 观察值的 getter、setter 的触发,然后执行 日志打印 任务
class Observer {
constructor(el) {
this.el = document.querySelector(el);
this.logPool = [];
}
updateLog(type, key, oldValue, newValue) {
switch (type) {
case 'get':
this.getProp(key, oldValue);
break;
case 'set':
this.setProp(key, oldValue, newValue);
break;
default:
break;
}
}
getProp(key, value) {
const o = {
type: 'get',
dateTime: new Date(),
key,
value
};
this.logPool.push(o);
this.log(o);
}
setProp(key, oldValue, newValue) {
const o = {
type: 'set',
dateTime: new Date(),
key,
oldValue,
newValue
};
this.logPool.push(o);
this.log(o);
}
log(o) {
const { type, dateTime, key } = o;
const oLi = document.createElement('li');
let htmlStr = '';
switch (o.type) {
case 'get':
htmlStr = `
${dateTime}:
I got the key '${key}'.
The value of the key is ${o.value};
`
break;
case 'set':
htmlStr = `
${dateTime}:
I set the key ${key}'s value '${o.newValue}'
from the old value '${o.oldValue}
`
break;
default:
break;
}
oLi.innerHTML = htmlStr;
this.el.appendChild(oLi);
console.log(this.logPool);
}
}
index.js
;(() => {
const target = new Target({
username: 'Lance123',
password: '123456',
age: 10,
gender: 'male'
});
const init = () => {
console.log(target.username);
target.age = 25;
}
init();
})();
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">按钮</button>
<script src="./PubAndSub.js"></script>
<script src="./index.js"></script>
</body>
</html>
PubAndSub.js
var oBtn = document.getElementById('btn');
// 订阅
oBtn.addEventListener('click', handler1, false);
oBtn.addEventListener('click', handler2, false);
oBtn.addEventListener('click', handler3, false);
// 监听到事件后,执行下面所有订阅了此事件的函数
function handler1() {
console.log('handler1');
}
function handler2() {
console.log('handler2');
}
function handler3() {
console.log('handler3');
}
class EventEmitter {
handlers = {
// 事件类型type: [订阅者1, 订阅者2, ...]
// e.g. : click: [handler1, handler2, handler3]
}
// type: click事件/change事件...
on(type, handler, once) {
if (!this.handlers[type]) {
this.handlers[type] = [];
}
if (!this.handlers[type].includes(handler)) {
this.handlers[type].push(handler);
handler.once = once;
}
}
once(type, handler) {
this.on(type, handler, false);
}
off(type, handler) {
if (this.handlers[type]) {
this.handlers[type] = this.handlers[type].filter(h => {
return h !== handler;
})
}
}
trigger(type) {
if (this.handlers[type]) {
this.handlers[type].forEach(handler => {
handler.call(this);
if (handler.once) {
this.off(type, handler);
}
});
}
}
}
const ev = new EventEmitter();
function handler1() {
console.log('handler1');
}
function handler2() {
console.log('handler2');
}
function handler3() {
console.log('handler3');
}
ev.on('test', handler1);
ev.on('test', handler2);
ev.on('test', handler3);
ev.trigger('test');