面试官:观察者模式在面向对象的语言中很常见...
我:阿巴阿巴阿巴(内心:没记啊,完了完了,我这leetcode不是白刷了)
graph TD
Subject["主题 (Subject)"]
Observer1["观察者1 (Observer)"]
Observer2["观察者2 (Observer)"]
Observer3["观察者3 (Observer)"]
Subject -->|注册| Observer1
Subject -->|注册| Observer2
Subject -->|注册| Observer3
Subject -->|通知| Observer1
Subject -->|通知| Observer2
Subject -->|通知| Observer3
可以看出:主题和观察者是关键两个角色,我个人喜欢第一张图,也更好理解一下。
我在记忆主题和观察者的时候就没有那么清晰,那么可以设想为两个具象的角色,比如主题为小宝宝,观察者为爸爸妈妈,一下子就有感觉了。
易于理解版
下面我结合代码来讲解:
// 定义主题(Subject)
class Subject {
constructor() {
this.observers = []; // 存储所有的观察者
}
// 注册观察者
addObserver(observer) {
this.observers.push(observer);
}
// 通知所有观察者
notify(message) {
this.observers.forEach(observer => observer.updated(message));
}
}
// 定义观察者(Observer)
class Observer {
constructor(name) {
this.name = name; // 观察者的名字
}
// 接收到通知后执行的操作
updated(message) {
if (message === '哭了') {
console.log(this.name + ':给宝宝喂奶');
} else {
console.log(this.name + ':看看宝宝是不是要换尿布了');
}
}
}
// 使用示例
const baby = new Subject(); // 创建主题
const father = new Observer('爸爸'); // 创建观察者:爸爸
const mother = new Observer('妈妈'); // 创建观察者:妈妈
baby.addObserver(father); // 注册爸爸为观察者
baby.addObserver(mother); // 注册妈妈为观察者
// 主题状态发生变化,通知所有观察者
baby.notify('哭了');
代码输出为:
爸爸:给宝宝喂奶
妈妈:给宝宝喂奶
注册观察者,我们可以假设成小宝宝只认爸爸妈妈,其他人喂奶就不喝!
代码的关键点:通知所有观察者用的是this.observer.forEach(observer => observer.update(message))
当然也有以公众号和用户来描述这种关系的,公众号为主题,用户为观察者,观察者需要有注册这么一个动作,然后就能接受到公众号发布的消息了。
以上主要是方便记忆的方式:一想到观察者模式,小宝宝爸爸妈妈
华为面试官:大概20行就能写出来
我:这...a...(开始瞎写)
标准版
下面我们手写正规的观察者模式:
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
this.observers = this.observers.filter((obs) => obs !== observer);
}
notify(data) {
this.observers.forEach((observer) => observer.updated(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
updated(data) {
console.log(this.name + ' 收到通知:' + data);
}
}
const subject = new Subject();
const observer1 = new Observer('观察者1');
const observer2 = new Observer('观察者2');
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notify('更新');
subject.removeObserver(observer1);;
subject.notify('再次更新');
其中filter:浅拷贝通过过滤的元素
新增了移除观察者
输出:
观察者1 收到通知:更新
观察者2 收到通知:更新
观察者2 收到通知:再次更新
当时写成这样估计能过了💔
扩展版
实际应用中,观察者模式可能会更复杂,可以加入一次性观察者、异步通知、优先级等
一次性观察者
一次性观察者可以通过在添加观察者时体现
addObserver(observer, once = false) {
this.observers.push({ observer, once });
}
notify(data) {
this.observers.slice().forEach((obs, index) => {
obs.observer.update(data);
// 如果是一次性观察者,则移除
if (obs.once) {
this.observers.splice(index, 1);
}
});
}
小tips:
forEach的语法为forEach(callbackFn, thisArg),因为使用了箭头函数,所以 thisArg 参数无关紧要,箭头函数没有自己的 this 绑定,callbackFn 接收3个参数,分别是
- element:数组当前正在处理的元素
- index:正在处理的元素在数组中的索引
- array:调用该方法的数组
前方预警!!!🕳️
在 forEach 前需要加入 .slice() 来进行浅拷贝,否则在迭代期间修改数组,会出现跳元素的情况
以下面例子为例:three 被跳过了
splice(拼接)的语法为 splice(start, deleteCount, item1, item2, /* …, */ itemN),而且是就地修改
可以通过如下方法设置一次性观察者
subject.addObserver(observer1);
subject.addObserver(observer2, true); // 设置为一次性观察者
异步处理
因为js是单线程语言,异步通知可以避免阻塞主线程,提高性能和响应速度等
notify(data) {
this.observers.forEach(observer => {
setTimeout(() => {
observer.update(data);
}, 0);
});
}
可以这样处理
优先级
优先级机制可以确保高优先级的观察者先被推送到,在任务调度等场景可以用到
/**
* 添加观察者
* @param {Observer} observer - 观察者实例
* @param {number} priority - 观察者的优先级(数值越高优先级越高)
*/
addObserver(observer, priority = 0) {
this.observers.push({ observer, priority });
// 按优先级从高到低排序
this.observers.sort((a, b) => b.priority - a.priority);
}
// 创建观察者
const observer1 = new Observer('观察者1');
const observer2 = new Observer('观察者2');
const observer3 = new Observer('观察者3');
// 添加观察者到被观察者,设置不同的优先级
subject.addObserver(observer1, 1); // 优先级1
subject.addObserver(observer2, 3); // 优先级3
subject.addObserver(observer3, 2); // 优先级2
输出顺序会变成观察者2、3、1
END
面试官最后又以伪代码给我讲了一遍观察者模式
谨以此文纪念我逝去的华子😭