面试官:“请问 vue 实现双向绑定的原理是什么?”
我:“是利用 Object.defineproperty 进行数据劫持,实现观察者模式......”
面试官:“是观察者模式么?”
我:“?是啊”
面试官:“那你能和我说一下观察者模式和发布订阅模式一样么?不一样的话区别又在哪?”
我:“???”
前言
不知道各位在面试中/被面试中有没有遇到以上的场景,观察者模式和发布订阅模式到底只是翻译不同,还是真的不是一个东西呢? vue 使用的又到底是哪种呢?让我们一起探讨一下。
为什么要使用观察者/发布订阅模式
假如你是一位托儿所的老师,当孩子们放学来到你这里时,你需要看着他们一个个的写作业,他们来自不同的学校,年纪也不尽相同,这让你的工作非常繁琐,就像:
const children = [
{
name: '张三',
homework: '三张数学卷子'
},
{
name: '李四',
homework: '一张数学卷子加两张语文卷子'
},
{
name: '王二麻子',
homework: '上课捣乱罚抄课本'
},
// ......
];
function yourJob() {
children.map(({homework}) => console.log(homework));
}
if(time === '17:00') {
yourJob();
}
“这样看上去好像还好嘛”
不过实际场景不可能这么简单,真实的情况很可能是:
const children = [
{
name: '张三',
homework: '三张数学卷子',
sport: '打一场篮球'
},
{
name: '李四',
homework: '一张数学卷子加两张语文卷子',
music: '学会唱鸡你太美'
},
{
name: '王二麻子',
homework: '上课捣乱罚抄课本',
doHomework: false,
playGame: '手游抽卡',
extendActivity: '计划明天上课的捣乱计划'
},
// ......
];
“哦我的天啊,考虑到每个孩子要做的不仅不同,每天都是变化的,就算我今天努力按照情况制定好计划,明天又要推倒重来!
要是这些孩子懂事点就好了,我就不用这么费心了......”
怎么才叫懂事呢?比起挨个盯着孩子们完成任务,如果一声令下,他们都能自己去完成自己该做的,是不是就太好了?
而以上的痴心妄想写出来就像这样:
const yourJobMap = (function () {
let children = [];
return {
doYourJob() {
children.forEach(({doOwnJob}) => doOwnJob());
},
comeIn(...args) {
children = children.concat(args);
},
getOut(...args) {
children = children.filter(child => !args.find(curChild => curChild === child));
}
};
})();
const {doYourJob, comeIn, getOut} = yourJobMap;
// 孩子们来了
comeIn(children1);
// 有的孩子又走了
getOut(children2);
// 通知在场的孩子们干活啦
doYourJob();
“这也太方便了!因为孩子们都懂事,自己知道自己要做什么,这样我只要通知他们开始工作就好,并不需要关心他们具体要做什么!”
正是如此,观察者也好,发布订阅也好,他们都解决了一个问题:对于多个不同对象基于同一个对象的变化而执行某些不同的操作时,如何更好的维护代码,也就是降低耦合——即实现对设计模式六大原则中 limit 原则(最少知道原则,尽量降低类与类之间的耦合)的体现。
什么是观察者模式
上述例子就是最简单的观察者模式,即观察者对象( observer,比如上述的孩子 )可以向某个主题( subject ,比如上述的托儿所)注册、注销等,当有事件发生的时候(到时间了,该做作业了),观察者可以接收到通知去进行自己对应的处理。
在前端实际生产中,最常用的观察者模式的实践应该是 EventListener 了吧:
// 简单的事件监听对象
const eventListener = (function () {
const events = {};
return {
// 增加监听事件
addEventListener(eventName, callback) {
if(events[eventName]) {
events[eventName].push(callback);
} else {
events[eventName] = [callback];
}
},
// 移除监听事件
removeEventListener(eventName, callback) {
if(events[eventName]) {
events[eventName] = events[eventName].filter(fun => fun !== callback);
} else {
events[eventName] = [];
}
},
// 触发事件
triggerEvent(eventName) {
if(event[eventName]) {
event[eventName].forEach(callback => callback());
}
}
};
})();
什么是发布订阅模式
Observer vs Pub-Sub
不得不说,如果仅仅是上述的观察者模式,已经足够应对我们的“托儿所”问题了,如果发布订阅模式和观察者模式不同的话,它和观察者模式的区别在哪呢?又是为了解决什么问题呢?
再用托儿所的例子举例,现在是疫情隔离期间,作为线下的托儿所没有办法只能歇业,老师没法接触到学生了,尽管孩子们懂事,但毕竟还是需要有人监督的,那老师该怎么做呢?
老师想到一个办法,他把所有孩子的家长拉进了一个群,每天定时在群里发消息,告诉家长现在孩子要写作业/要锻炼了,然后再由家长面对面地监督孩子。渐渐的他发现,他并不知道哪些孩子真的收到了他的指令去行动了,孩子们也不知道老师到底通知了些什么,老师和孩子,仿佛从没接触过,只有家长在负责两头传话。
如果说观察者模式中, Subject 和 Observer 是一种松耦合状态,那在发布订阅模式中, Publisher 和 Subscriber 就是解耦的,它们两者之间的联系全部通过家长(?)来实现。
什么是“家长”
在上述举例中的“家长”角色在生活中无处不在,购物链中的超市,企业中的hr等等,毕竟这个世界总是有“中间商赚差价”的。
这里就要引入另一个设计模式——代理模式
了。
这个命名非常贴切,就像明星的经纪人一样,你看到的明星都是经纪人包装过的样子,黑粉的言语也会被经纪人公关处理,尽量不会让明星本人看到。你看似每天在微博上和你的偶像互动,实际上你和你的偶像是完全解耦的 hhh......
在实际生产中,es6已经为前端开发者提供了一套代理模式的 API —— Proxy ,大家可以通过 Proxy API 实际感受下代理模式:
const Angelababy = {
name: '杨颖',
age: '35',
fansCount: 5000000
};
const AgentOfAb = new Proxy(Angelababy, {
get(star, key) {
const value = star[key];
switch(key) {
case 'age':
return `${value - 5}岁`;
default:
return value;
}
},
set(star, key, value) {
switch(key) {
case 'fansCount':
Reflect.set(target, key, value * 10);
default:
Reflect.set(target, key, value);
}
}
});
真正的发布订阅模式
我们已经讨论过托儿所如何从观察者模式发展成发布订阅模式了,为了让“托儿所”与“孩子”们之间实现从松耦合变为解耦,我们带入了“家长”,所以真正的发布订阅模式应该是:
发布订阅模式
= 观察者模式
+ 代理模式
所以现在的托儿所已经变成如下这种模式了:
class Publisher {
constructor(proxy) {
this.observer = proxy;
}
doYourJob(jobName) {
this.observer.watchYourChildren(jobName);
}
}
class Watcher {
constructor(children) {
this.observer = children;
}
addChild(child) {
this.observer.push(child);
}
removeChild(curChild) {
this.observer = this.observer.filter(child => child !== curChild);
}
watchYourChildren(jobName) {
this.observer.forEach(child => childDoSomething(jobName, child));
},
childDoSomething(jobName, child) {
switch(jobName) {
case 'doHomework':
case 'sport':
child.doHomework();
return;
case 'sleep':
child.rest();
setTimmeout(() => child.doHomework(), 60000);
return;
case 'music':
child.listen('English Listening');
return;
default:
return;
}
}
}
const children = [
// 可怜的孩子们
];
// 狠毒的家长
const parent = new Watcher(children);
// 啥也不知道的老师
const yourJob = new Publisher(parant);
// 你以为孩子在做
yourJob.doYourJob('doHomework');
yourJob.doYourJob('sport');
yourJob.doYourJob('sleep');
// 实际上孩子在做
// homework homework homework
Vue使用的设计模式到底是?
上面是 Vue 双向绑定的原理,我们可以清楚地看出,数据和视图并不是耦合的,而是由 Watcher 去处理两边的状态变化, Vue 中使用的正是发布订阅模式。使用这种模式不仅可以让数据的变化可以实时反映在视图上,更让 Vue 有了获取数据变化( watch ),处理数据再展示( computed ),异步变化数据等等仅靠观察者无法做到的事。
小结
其实对于观察者模式和发布订阅模式的关系与区别众说纷纭,这方面的理解也很多元,以上只是我对于自己理解的一番阐述,希望大家和平探讨,尤其是和面试候选人哦。
参考: https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c