发布—订阅模式运用在 Vue2 的双向数据绑定中,想要将双向数据绑定了解透彻,个人觉得有必要首先理解一下这个开发模式。以下是我看有关开发模式的书后写下的知识点。
发布—订阅模式又叫观察者模式(对此有不同说法),它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于他的对象都将得到通知。在 JavaScript 中,我们一般用事件模型来替代传统的发布—订阅模式(这句话有待理解,不知道可不可以理解为视图发生改变更新数据可以使用事件监听的方式实现)。
这里有一个现实中的小例子来帮助我们去理解发布—订阅模式。小明看上了一套房子,但是售楼处告知此楼盘的房子已售罄。而不久后会有一些尾盘推出,开发商手续办好即可购买,但是具体时间目前无法知道。
现小明有两种获取售楼消息的方式:1、每天给售楼出打电话。2、将电话留给售楼处,并告知自己想要购买的户型,有相应的房子就联系他。
毫无疑问选择第二种,如果还有小红,小王想要订阅某些消息也只需要留下自己的电话号码和需要的户型。售楼处可根据他们的需要和电话发布不同的信息通知他们。这就是发布订阅模式。
购房者无需天天给售楼处打电话询问,在合适的时间点,售楼处作为发布者会通知这些消息的订阅者。
说明这种模式可以广泛应用于异步编程中,这是一种代替回调函数的方案。在异步编程中使用发布订阅者模式,我们无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。
发布订阅双方之间不再强耦合在一起,订阅者是否需要订阅只需留下订阅信息,发布者的某些变动也不会影响到订阅者。
这点说明发布订阅模式可以取代对象之间硬编码的通知机制(这是什么?),一个对象不再需要显式调用另外一个对象的某个接口。两个对象松耦合地联系在一起,彼此不需要清除对方内部地实现细节。
1、简单的发布订阅模式原理代码如下。可以定义一个发布者对象并为其定义一些属性方法,例如存放订阅者信息的列表、添加订阅者的方法和发布消息的方法等。
// 发布——订阅者模式
// 定义发布者对象
var publisher = {}
// 缓存列表,存放订阅者的回调函数
publisher.subscriberList = [];
// 添加新的订阅者,一次可添加多个,参数是各个函数
publisher.addSubscriber = function() {
[...arguments].forEach(item => {
// 将订阅消息添加进缓存列表
this.subscriberList.push(item)
})
}
// 发布消息
publisher.trigger = function() {
this.subscriberList.forEach(callback => {
// arguments为发布消息时带上的参数
callback(...arguments)
})
}
// 测试
// 小明想要订阅的消息(定义一个订阅者小明)
function xiaoMing(job, salary) {
console.log('职位:' + job);
console.log('薪资:' + salary);
}
// 小红想要订阅的消息(定义另一个订阅者小红)
function xiaoHong(job, salary) {
console.log('职位:' + job);
console.log('薪资:' + salary);
}
publisher.addSubscriber(xiaoMing, xiaoHong);
// 发布者发布消息
publisher.trigger('前端开发', 6000);
2、不同的订阅者也许感兴趣的是不同类型的消息,就像 promise 对象中的 catch 方法只对异常的消息感兴趣。而这里的小明想要了解的岗位只是 ‘web’ ,而发布者在发布 ‘sever’ 类的消息时,他并不希望收到。所以此时可以将保存订阅者信息的数组列表改为使用保存订阅各种类型消息的数组的对象。并相应改进订阅发布的方法。
// 发布——订阅者模式(可以发布订阅不同的消息)
// 定义发布者
var publisher = {}
// 缓存列表,此时使用对象方便区分消息类型
publisher.subscriberList = {};
// 添加新的订阅者和传入其订阅消息类型的key值
// 这里为了简化一点,所以一次就只能添加一个订阅者
publisher.addSubscriber = function(key, fn) {
// 如果还没有人订阅过此类消息,给该消息创建一个缓存列表
if (!this.subscriberList[key]) {
this.subscriberList[key] = [];
}
this.subscriberList[key].push(fn);
}
// 发布某种类型的消息
publisher.trigger = function() {
// 取出所有的订阅者
var key = Array.prototype.shift.call(arguments);
this.subscriberList[key].forEach(fn => {
fn(...arguments);
});
}
// 测试
// 小明想要订阅的消息(定义一个订阅者小明)
function xiaoMing(job, salary) {
console.log('小明职位:' + job);
console.log('薪资:' + salary);
}
// 小红想要订阅的消息(定义另一个订阅者小红)
function xiaoHong(job, salary) {
console.log('小红职位:' + job);
console.log('薪资:' + salary);
}
publisher.addSubscriber('web', xiaoMing);
publisher.addSubscriber('sever', xiaoHong);
// 发布者发布消息
// publisher.trigger('web', '前端开发', 6000);
publisher.trigger('sever', '后端开发', 7000);
3、如果其他的对象也希望自己能有发布者功能,为了再次复用上面的代码,可以封装一个专门为对象动态添加发布职责的函数。
// 发布——订阅者模式(给对象动态添加发布订阅者功能)
var event = {
subscriberList: {},
addSubscriber: function(key, fn) {
// 如果还没有人订阅过此类消息,给该消息创建一个缓存列表
if (!this.subscriberList[key]) {
this.subscriberList[key] = [];
}
this.subscriberList[key].push(fn);
},
trigger: function() {
// 取出所有的订阅者
var key = Array.prototype.shift.call(arguments);
this.subscriberList[key].forEach(fn => {
fn(...arguments);
});
}
}
var installEvent = function(obj) {
for (let k in event) {
obj[k] = event[k];
}
}
var publisher = {}
installEvent(publisher)
// 测试
// 小明想要订阅的消息(定义一个订阅者小明)
function xiaoMing(job, salary) {
console.log('小明职位:' + job);
console.log('薪资:' + salary);
}
// 小红想要订阅的消息(定义另一个订阅者小红)
function xiaoHong(job, salary) {
console.log('小红职位:' + job);
console.log('薪资:' + salary);
}
publisher.addSubscriber('web', xiaoMing);
publisher.addSubscriber('sever', xiaoHong);
// 发布者发布消息
// publisher.trigger('web', '前端开发', 6000);
publisher.trigger('sever', '后端开发', 7000);
4、某个订阅者可能在某时不需要订阅某类消息,因此发布者需要提供一个取消订阅的方法。
// 发布——订阅者模式(取消某类消息的所有订阅或者特定订阅)
var event = {
subscriberList: {},
addSubscriber: function(key, fn) {
// 如果还没有人订阅过此类消息,给该消息创建一个缓存列表
if (!this.subscriberList[key]) {
this.subscriberList[key] = [];
}
this.subscriberList[key].push(fn);
},
trigger: function() {
// 取出所有的订阅者
var key = Array.prototype.shift.call(arguments);
this.subscriberList[key].forEach(fn => {
fn(...arguments);
});
}
}
// 取消订阅
event.remove = function(key, fn) {
// 获取该类消息的订阅列表
var fns = this.subscriberList[key];
// 如果没有订阅该类消息的订阅者,直接返回
if (!fns) {
return false;
}
// 没有传入具体的订阅者信息fn则取消该类消息所有订阅
if (!fn) {
fns && (fns.length = 0);
return true;
};
// 取消特定消息的特定订阅,可以正向遍历也可以反向遍历订阅列表
fns.some((cb, i) => {
if (cb === fn) {
// 取消相应的订阅
fns.splice(i, 1);
return true;
} else {
return false;
}
})
}
var installEvent = function(obj) {
for (let k in event) {
obj[k] = event[k];
}
}
var publisher = {}
installEvent(publisher)
// 测试
// 小明想要订阅的消息(定义一个订阅者小明)
function xiaoMing(job, salary) {
console.log('小明职位:' + job);
console.log('薪资:' + salary);
}
// 小红想要订阅的消息(定义另一个订阅者小红)
function xiaoHong(job, salary) {
console.log('小红职位:' + job);
console.log('薪资:' + salary);
}
publisher.addSubscriber('web', xiaoMing);
publisher.addSubscriber('sever', xiaoHong);
// 取消某个订阅
publisher.remove('web', xiaoMing);
// 此时小明是无法在接收到发布的web类消息
publisher.trigger('web', '前端开发', 6000);
publisher.trigger('sever', '后端开发', 7000);
5、动态添加多个发布对象时他们的方法相同但是并不公用内存资源,所以比较浪费内存空间,我们可以定义一个全局的发布者对象。
// 发布——订阅者模式(定义一个全局的发布者,即设置一个观察者,不需要理会发布者是谁,只定义一个全局发布对象)
// 使用立即执行函数的方式
var Event = (function() {
// 定义订阅列表和发布——订阅方法
var subscriberList = {},
addSubscriber,
trigger,
remove;
addSubscriber = function(key, fn) {
if (!subscriberList[key]) {
subscriberList[key] = [];
}
subscriberList[key].push(fn);
};
trigger = function() {
// 取出所有的订阅者
var key = Array.prototype.shift.call(arguments);
subscriberList[key].forEach(fn => {
fn(...arguments);
});
};
remove = function(key, fn) {
var fns = subscriberList[key];
if (!fns) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
return true;
};
fns.some((cb, i) => {
if (cb === fn) {
fns.splice(i, 1);
return true;
} else {
return false;
}
})
};
/*
*返回一个全局的发布者对象,这里会产生闭包(因为立即执行函
* 数里面有函数以返回对象方法的方式进行了返回)
*/
return {
addSubscriber,
trigger,
remove
}
})()
// 测试
// 小明想要订阅的消息(定义一个订阅者小明)
function xiaoMing(job, salary) {
console.log('小明职位:' + job);
console.log('薪资:' + salary);
}
// 小红想要订阅的消息(定义另一个订阅者小红)
function xiaoHong(job, salary) {
console.log('小红职位:' + job);
console.log('薪资:' + salary);
}
Event.addSubscriber('web', xiaoMing);
Event.addSubscriber('sever', xiaoHong);
// 取消某个订阅
Event.remove('web', xiaoMing);
// 此时小明是无法在接收到发布的web类消息
Event.trigger('web', '前端开发', 6000);
Event.trigger('sever', '后端开发', 7000);
6、这算是难点的一部分了,也就是解决发布和订阅的顺序必须为先订阅后发布的问题和全局事件的命名冲突问题。。
1)可以看到以上方式中订阅均需要发生在发布之前,订阅者才能够收到发布的该信息(这也许和这里场景还算匹配)。但是在现实开发,特别是异步任务时,有可能在订阅之前已经实现了消息的发布(异步任务的消息的发布时间是无法预知的)。所以我们在此需要解决这个问题,在订阅前的发布,订阅者依旧能够接收一次发布的信息。
2)解决上述问题的方案:建立一个离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时将发布消息的事件包裹在一个函数中并将该函数存放入堆栈。当有对象来订阅此消息时,遍历堆栈并一次执行离线事件,即进行消息的发布,当然离线事件的生命周期只有一次。
3)上面的实现都只使用了一个对象去保存消息类型和订阅者信息,当订阅内容过多时,可能会出现一些命名冲突,所以也需要使用多个对象(命名空间)。
这个是书上的代码,有点复杂。书上一个字的注释都没有,看起来还是挺吃力的,建议首先从函数方法的调用部分看起,这样比较容易有思路。
// 发布——订阅者模式(解决全局事件的命名冲突问题)
// 发布——订阅者模式(解决全局事件的命名冲突问题)
// 外层的Event变量是一个立即执行函数(下面简称outsider)返回的结果
var Event = (function() {
// 立即执行函数outsider的this
var global = this,
// outsider里面的Event变量
Event,
// outsider里面的_default变量默认值是'_default',这主要是用来标识命名空间的一个key
_default = 'default';
// 内层的Event变量也是一个立即执行函数(下面简称insider)的返回
Event = function() {
// insider内部的一些变量和函数(私有属性和方法)
var _listen,
_trigger,
_remove,
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
// 命名空间缓存对象,用来保存所有的命名空间(订阅信息对象)的一个对象
namespaceCache = {},
_create,
find;
/*
* each方法:传入一个数组,遍历里面的元素
* 使用里面的每个元素都使用call方法调用一次传入的回调函数
* 返回值赋值给 ret, 将最后的ret结果返回
*/
var each = function(ary, fn) {
var ret;
for (var i = 0, l = ary.length; i < l; i++) {
var n = ary[i];
// 调用时传入的参数有元素的索引和元素对象本身
ret = fn.call(n, i, n);
}
return ret;
};
// 添加订阅需要传递的三个参数分别代表消息类型,订阅者(函数)信息和所在
// 的命名空间对象(namespaceCache的某个属性,也是一个对象)
_listen = function(key, fn, cache) {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
};
// 取消订阅
_remove = function(key, cache, fn) {
if (cache[key]) {
if (fn) {
cache[key].some((fns, i) => {
if (fns === fn) {
cache[key].splice(i, 1);
return true
} else {
return false;
}
})
} else {
cache[key] = []
}
}
};
// 发布消息
_trigger = function() {
// 发布的消息所在的命名空间
var cache = _shift.call(arguments);
// 消息类型
var key = _shift.call(arguments);
var _self = this,
// 消息携带的参数
args = arguments,
ret,
// 订阅列表
stack = cache[key];
// 没有人订阅直接返回
if (!stack || !stack.length) {
return;
};
/*
* 1、每个订阅者函数作为this调用一次each中的回调函数
* 2、即这里面的this指向订阅者函数对象
* 3、这里面的_self和args是指向发布者对象和发布消息携带的参数
* 4、因此也就是在发布者发布方法中调用订阅者函数并传入参数
*/
return each(stack, function() {
return this.apply(_self, args);
})
};
// 重难点
_create = function(namespace) {
// 如果没有提供新命名空间key默认使用'_default'
var namespace = namespace || _default;
// 订阅者缓存列表
var cache = {},
// 离线事件列表
offlineStack = [];
// 创建的发布者对象,最后可能会返回,如果已经存在则不返回
var ret = {
listen: function(key, fn, last) {
// 添加订阅消息
_listen(key, fn, cache);
// 如果离线事件列表为空则返回
if (offlineStack === null) {
return
}
// 如果存在离线事件且last === 'last'
if (last === 'last') {
// 执行最后一个离线事件
offlineStack.length && offlineStack.pop()();
} else {
// 调用 each,执行所有的离线事件
each(offlineStack, function() {
this();
});
}
// 销毁离线列表
offlineStack = null;
},
// 先将订阅消息进行取消操作再次订阅需要带上last参数
one: function(key, fn, last) {
_remove(key, cache);
this.listen(key, fn, last);
},
// 取消订阅消息
remove: function(key, fn) {
_remove(key, cache, fn);
},
// 发布消息
trigger: function() {
var fn,
args,
_self = this;
// 将某个命名空间的 消息列表添加到trigger的参数前面
_unshift.call(arguments, cache);
args = arguments;
fn = function() {
return _trigger.apply(_self, args);
};
// 存在离线任务列表,将新的离线任务添加至离线任务例表
if (offlineStack) {
return offlineStack.push(fn);
}
// 返回离线任务
return fn();
}
};
if (namespace) {
if (namespaceCache[namespace]) {
return namespaceCache[namespace]
} else {
return namespaceCache[namespace] = ret;
}
} else {
return ret;
};
};
// insider返回的一个发布者对象
return {
create: _create,
one: function(key, fn, last) {
var event = this.create();
event.one(key, fn, last);
},
remove: function(key, fn) {
var event = this.create();
event.remove(key, fn);
},
listen: function(key, fn, last) {
var event = this.create();
event.listen(key, fn, last);
},
trigger: function() {
var event = this.create();
event.trigger.apply(this, arguments)
}
};
}();
// outsider直接将insider的返回也进行了返回
return Event;
})();
Event.trigger('click', 1);
Event.listen('click', function(a) {
console.log(a);
});
Event.create('name1').listen('click', function(b) {
console.log(b);
})
Event.create('name1').trigger('click', 2);
Event.create('name2').listen('click', function(c) {
console.log(c);
})
Event.create('name2').trigger('click', 3);
发布—订阅模式的优势是实现时间和对象之间的解耦。但也有缺点,创建订阅者需要消耗一定的时间和内存,即使是订阅的消息永远都没有发布,订阅者始终也会存在于内存中,还有过多使用这种模式会过度弱化对象之间的联系,导致程序难以跟踪维护和理解。