以下内容来源于《JavaScript设计模式与开发实践》中的第八章——发布订阅模式。只是修改了部分内容(比如var改为let/const、一些函数的位置、变量名等),方便大家看清逻辑、消化、吸收。
全局的发布订阅对象
const globalEvent = (function () {
const listener = {};
const listen = (key, fn) => {
if (!listener[key]) {
listener[key] = [];
}
listener[key].push(fn);
}
const remove = (key, fn) => {
const fns = listener[key];
if (!fns) {
// 如果 key 对应的消息没有被人订阅,则直接返回
return;
}
if (!fn) {
fns = []
return;
}
for (let i = fns.length - 1; i >= 0; i--) {
// 反向遍历订阅的回调函数列表
const _fn = fns[ i ];
if (_fn === fn) {
fns.splice(i, 1);
}
}
}
const trigger = (key, val) => {
const fns = listener[key];
if (!fns || fns.length === 0) {
return;
}
for (let i = 0, fn; (fn = fns[i++]); ) {
fn(val);
}
}
return {
listen,
remove,
trigger,
}
}());
globalEvent.listen('squareMeter88', (fn1 = function (price) {
console.log('价格= ' + price);
})
);
globalEvent.listen('squareMeter88', (fn2 = function (price) {
console.log('价格= ' + price);
})
);
globalEvent.remove('squareMeter88', fn1); // 删除订阅
globalEvent.trigger('squareMeter88', 2000000); // 输出:2000000
跟通用模式相比,就是多了个闭包,然后只暴露出三个方法给外层,clientList等于是闭包内是私有变量,外层访问不到。
全局对象,先订阅后发布,并且加了命名空间的处理
const eventGlobal = (function () {
const event = (function() {
const nameSpaceMap = {};
const defaultName = '_default_';
const _listen = (key, cache, fn) => {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
}
const _remove = (key, cache, fn) => {
const fns = cache[key];
if (!fns || !fns.length) {
return;
}
if (!fn) {
fns = [];
return;
}
for (let i = fns.length - 1; i >= 0; i--) {
const _fn = fns[i];
if (_fn === fn) {
fns.splice(i, 1);
}
}
}
const _trigger = (key, cache, val) => {
const fns = cache[key] || [];
each(fns, function() {
this.call(null, val);
})
}
const each = (fns, fn) => {
fns.map(_fn => {
fn.call(_fn);
})
}
const _create = (namespace) => {
const name = namespace || defaultName;
const cache = {};
let offlineStack = [];
const ret = {
create: _create,
listen: (key, fn) => {
_listen(key, cache, fn);
if (!offlineStack) {
return;
}
each(offlineStack, function() {
this()
});
offlineStack = null;
},
remove: (key, fn) => _remove(key, cache, fn),
trigger: (key, val) => {
const fn = () => _trigger(key, cache, val);
if (!offlineStack) {
return fn();
}
offlineStack.push(fn);
}
}
return nameSpaceMap[name] || (nameSpaceMap[name] = ret);
}
return {
create: _create,
listen: function (key, fn) {
const event = _create();
event.listen(key, fn);
},
remove: function (key, fn) {
const event = _create();
event.remove(key, fn);
},
trigger: function (key, fn) {
const event = _create();
event.trigger(key, fn);
},
}
} ());
return event;
} ())
/************** 先发布后订阅 ********************/
eventGlobal.trigger("click", 1);
eventGlobal.trigger("click", 12);
eventGlobal.listen("click", function (a) {
console.log(a); // 输出:1
});
/************** 使用命名空间 ********************/
eventGlobal.create("namespace1").trigger("click", 8);
eventGlobal.create("namespace1").listen("click", (fn1 = function (a) {
console.log(a); // 输出:1
}));
eventGlobal.create("namespace1").remove("click", fn1);
eventGlobal.create("namespace1").trigger("click", 10);
小结
发布—订阅模式在实际开发中非常有用。发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可 以用来帮助实现一些别的设计模式,比如中介者模式。 从架构上来看,无论是MVC 还是MVVM, 都少不了发布—订阅模式的参与,而且JavaScript 本身也是一门基于事件驱动的语言。 当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联 系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个bug 不是件轻松的事情。
彩蛋
这里存在了一个经典的js问题,即回调函数this的隐式绑定,及绑定丢失的问题。有关this的深度剖析,请查阅《你不知道的JavaScript(上卷)》第二部分的第二章——this的全面解析。
each(getAvt, getName); // 控制台输出:111111111
function each(n, fn) {
fn.call(n);
}
function getAvt() {
console.log('111111111');
}
function getName() {
this();
}
call函数的使用,改变了getName里的函数this的指向,所以这个时候调用this()即为调用getAvt函数本身。 通常我们使用的时候是后面跟着一个对象,这里跟着一个函数,函数也继承自对象,所以这里应该是绑定一个内存地址。
拓展《你不知道的JavaScript(上卷)》里的经典关于this变化的例子
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
虽然bar 是obj.foo 的一个引用,但是实际上,它引用的是foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。