今天第六天,剛好是禮拜天,冷冷的夜晚,睡前看一下Design Pattern,不只暖你的胃更暖你的心。
其實我真的很想把這篇拆成上中下三集,或是1-7像哈利波特一樣,假日寫文章真是要命
我們繼續用實際範例講解發佈訂閱模式,今天我們要做一個網站,登入的時候要依照使用者的資訊做一些畫面更新,例如:更新使用者頭像、更新資訊列表、購物車更新...等事情,比如像這樣寫:
login.success(function (data) {
head.setAvatar(data);
nav.setAvatar(data);
cart.refresh();
...
});
然後,今天老闆突然決定登入之後,儀表板畫面也要更新,所以只好找到之前寫的上面那段code再加入一段
dashboard.refresh();
其實像我們公司是以功能來分配給工程師工作,假如你又是負責登入功能的話,現在這件事也許有些地方會困擾你,像是必須去了解每個功能的指令例如head裡的setAvatar,以及我這段登入成功也太多事情了。
所以我們可以用發佈/訂閱模式改造一下:
首先當我們登入成功的時候我們只要發佈一個登入成功的消息就好
$.ajax('http:....', function (data) {
login.trigger('loginSuccess', data);
});
那剩下的事呢?當然交給個功能開發或是維護的人自己做啦,像這樣:
var header = (function () {
login.listen('loginSuccess', function (data) { header.setAvatar(data) });
return {
setAvatar: function (data) {
console.log('設置頭像');
}
}
})();
又臨時增加需求的話就只要在各自功能增加就可以囉:
var dashboard = (function () {
login.listen('loginSuccess', function (data) { dashboard.refresh(data) });
return {
refresh: function (data) {
console.log('dashboard refresh');
}
}
})();
總結以上(包含上篇)這些寫法可能有些問題,每個發佈者都要添加listen、trigger、clientList這些方法,感覺有點重複有點浪費。我們可以試著做全域的物件:
做法其實跟之前的內容很像,只要先做一個全域的物件,內容回傳實際執行方法:
//做一個全域的物件
var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
//實際執行方法(內容都跟上述的範例一樣)
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
trigger = function () {
var key = Array.prototype.shift.call(arguments);
var fnPool = clientList[key];
if (!fnPool || fnPool.length === 0) {
return false;
}
for (var i = 0, fn; fn = fnPool[i++];) {
fn.apply(this, arguments);
}
};
remove = function (key, removeFn) {
var fnPool = this.clientList[key];
if (!fnPool || fnPool.length === 0) {
return false;
}
if (!removeFn) {
fnPool && (fnPool.length = 0);
} else {
for (var i = 0, fn; fn = fnPool[i]; i++) {
if (fn === removeFn) {
fnPool.splice(i, 1);
}
}
}
};
//回傳給外面呼叫
return {
listen: listen,
trigger: trigger,
remove: remove
}
})();
我們實際用前端網頁來示範如何使用,兩個物件模組可以在保持封裝特性前提下進行通訊:
(function () {
createButtonA();
createDivB();
function createButtonA() {
var count = 0;
$('<button>').text('A').appendTo('body').click(function () {
Event.trigger('add', ++count);
});
}
function createDivB() {
var b = $('<div>').text('').appendTo('body');
Event.listen('add', function (count) {
b.text(count);
});
}
})();
那這樣的全域事件用久了一定會出現事件名稱衝突的狀況,所以我們可以給這個Event提供建立命名空間的功能,以及若先發佈再訂閱也要可以收到發佈的訊息:
var Event = (function () {
//預設命名
var _default = 'default';
var Event = function () {
var _slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {};
//實作發佈訂閱等內容
var _listen = function (key, fn, cache) {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
};
var _remove = function (key, cache, fn) {
if (cache[key]) {
if (fn) {
for (var i = cache[key].length; i >= 0; i--) {
if (cache[key] === fn) {
cache[key].splice(i, 1);
}
}
} else {
cache[key] = [];
}
}
};
var _trigger = function () {
var self = this;
var cache = _shift.call(arguments);
var key = _shift.call(arguments);
var args = arguments;
var fnPool = cache[key];
if (!fnPool || !fnPool.length) {
return;
}
return fnPool.forEach(function (fn) {
fn.apply(self, args);
});
};
//實作命名空間
var _create = function (namespace) {
var namespace = namespace || _default;
var cache = {},
offlineStack = [],
ret = {
listen: function (key, fn, last) {
_listen(key, fn, cache);
if (offlineStack === null) {
return;
}
if (last === 'last') {
offlineStack.length && offlineStack.pop()();
} else {
offlineStack.forEach(function (fn) {
fn();
});
}
offlineStack = null;
},
remove: function (key, fn) {
_remove(key, cache, fn);
},
trigger: function () {
var fn, args, self = this;
_unshift.call(arguments, cache);
args = arguments;
fn = function () {
return _trigger.apply(self, args);
};
if (offlineStack) {
return offlineStack.push(fn);
}
return fn();
}
};
return namespace ? (namespaceCache[namespace] ?
namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
}
/*
回傳實際使用介面,每次使用發佈、訂閱或是刪除其實都是先進到使用命名空間的函數裡,
取得相對應的命名空間之後,再回傳相對應的函數
*/
return {
create: _create,
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);
}
}
}()
return Event;
})();
那實際使用像這樣:
//可以先發佈在訂閱一樣可以收到
Event.create('company1').trigger('test', 1);
Event.create('company1').listen('test', function (a) {
console.log(a);
});
以上就是發佈/訂閱模式啦