介紹
最近開始給自己每週訂個學習任務,學習結果反饋為一篇文章的輸出,做好學習記錄。
這一週(02.25-03.03)我定的目標是《JavaScript 模式》的第七章學習一遍,學習結果的反饋就是本篇文章啦。
由於內容實在太長,我將本文分為兩部分:
- 《JavaScript 模式》知識點整理(上)
- 《JavaScript 模式》知識點整理(下)
本文內容中主要參考《JavaScript 模式》,其中也有些案例是來自網上資料,有備註出處啦,如造成不便,請聯絡我刪改。
過兩天我會把這篇文章收錄到我整理的知識庫 【Cute-JavaScript】 中,並已經同步到 【github】上面。
六.外觀模式(Facade Pattern)
1.概念介紹
**外觀模式(Facade Pattern)**是一種簡單又常見的模式,它為一些複雜的子系統介面提供一個更高階的統一介面,方便對這些子系統的介面訪問。
它不僅簡化類中的介面,還對介面和呼叫者進行解耦,外觀模式也常被認為是開發者必備,它可以將一些複雜操作封裝起來,並建立一個簡單介面用於呼叫。
2.優缺點和應用場景
2.1優點
- 輕量級,減少系統相互依賴。
- 提高靈活性。
- 提高了安全性。
2.2缺點
- 不符合開閉原則,如果要改東西很麻煩,繼承重寫都不合適。
2.3應用場景
- 為複雜的模組或子系統提供外界訪問的模組。
- 子系統相對獨立。
- 預防低水平人員帶來的風險。
- 專案重構。
3.基本案例
經常我們在處理一些特殊情況的時候,需要一起呼叫好幾個方法,我們使用外觀模式,就可以將多個方法包裝成一個方法,哪裡需要使用直接呼叫這個包裝好的方法就可以。
比如我們經常處理瀏覽器事件,需要同時呼叫stopPropagation()
和preventDefault()
,於是我們就可以新建一個外觀方法,實現這兩個方法同時呼叫:
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
複製程式碼
然後我們也可以使用外觀模式,來做IE事件的相容性:
let myEvent = {
// ...
stop: e => {
// 其他
if(typeof e.preventDefault === 'function'){
e.preventDefault();
}
if(typeof e.stopPropagation === 'function'){
e.stopPropagation();
}
// IE
if(typeof e.returnValue === 'boolean'){
e.returnValue = false;
}
if(typeof e.cancelBubble === 'boolean'){
e.cancelBubble = true;
}
}
};
複製程式碼
七、代理模式(Proxy Pattern)
1.概念介紹
代理模式(Proxy Pattern) 為其他物件提供一種代理,來控制這個物件的訪問,代理是在客戶端和真實物件之間的介質。
簡單的理解:如我們需要請明星來做廣告,我們會先通過聯絡Ta的經紀人,談好條件才會給明星籤合同。
2.優缺點和應用場景
2.1優點
- 職責單一且清晰。
- 保護真實物件。
- 開閉原則,高擴充性。
2.2缺點
- 由於在客戶端和真實物件間新增代理物件,導致請求處理速度變慢。
- 實現代理模式需要額外工作,有些代理模式實現起來非常複雜。
2.3應用場景
- 需要隱藏或保護某個類,則為這個類新增代理。
- 需要給不同訪問者提供不同許可權,則在代理類上做判斷。
- 需要為某個類新增功能,如新增日誌快取等,我們可以在代理的類做新增,而不管去改原來封裝好的類。
3.基本案例
這裡我們以吃午飯問題來學習代理模式。通常情況下,我們會有兩種方式解決午飯問題:“去餐廳吃”和“叫外賣”。
去餐廳吃的話,我們就是自己過去吃飯了唄,如果是叫外賣,我們就會通過外賣小哥來拿到午飯才能吃起來。
- 去餐廳吃(沒有使用代理模式)
// 定義午飯類 引數 菜名
let Lunch = function(greens){
this.greens = greens;
}
Lunch.prototype.getGreens = function(){
return this.greens;
}
// 定義我這個物件
let leo = {
buy: function(greens){
console.log(`午飯吃${greens.getGreens()}`);
}
}
// 去餐廳吃
leo.buy(new Lunch('青椒炒肉')); // 午飯吃青椒炒肉
複製程式碼
- 叫外賣(有使用代理模式)
// 定義午飯類 引數 菜名
let Lunch = function(greens){
this.greens = greens;
}
Lunch.prototype.getGreens = function(){
return this.greens;
}
// 定義外賣小哥這個物件
let brother = {
buy: function(lunch){
leo.buy(lunch.getGreens());
}
}
// 定義我這個物件
let leo = {
buy: function(greens){
console.log(`午飯吃${greens}`);
}
}
// 叫外賣
brother.buy(new Lunch('青椒炒肉')); // 午飯吃青椒炒肉
複製程式碼
並且外賣小哥還會幫我們做一些其他事,比如幫我們帶瓶可樂,我們改造brother
和leo
這2個物件,再看看效果:
let brother = {
buy: function(lunch){
if(leo.needCola) leo.buyCola();
leo.buy(lunch.getGreens());
}
}
let leo = {
needCola: true,
buy: function(greens){
console.log(`午飯吃${greens}`);
},
buyCola: function(){
console.log(`順手買瓶可樂!`);
}
}
brother.buy(new Lunch('青椒炒肉'));
// 順手買瓶可樂!
// 午飯吃青椒炒肉
複製程式碼
4.保護代理
還是借用 3.基本案例 的叫外賣的例子,我們現在要實現保護代理,而我們需要外賣小哥為了我們的身體健康,超過晚上9點,就不幫我們買可樂。
還是改造上面買可樂的brother
物件程式碼:
let brother = {
buy: function(lunch){
let nowDate = new Date();
if(nowDate.getHours() >= 21){
console.log('親,這麼晚不要喝可樂喲!');
}else{
if(leo.needCola) leo.buyCola();
leo.buy(lunch.getGreens());
}
}
}
brother.buy(new Lunch('青椒炒肉'));
// 順手買瓶可樂!
// 午飯吃青椒炒肉
複製程式碼
5.虛擬代理
虛擬代理能把一些開銷大的物件,延遲到真正需要的時候才去建立和執行。
我們這裡舉個圖片懶載入的例子:
這個案例參考自JS設計模式-代理模式.
// 圖片載入
let ele = (function(){
let node = document.createElement('img');
document.body.appendChild(node);
return{
setSrc : function(src){
node.src = src;
}
}
})()
// 代理物件
let proxy = (function(){
let img = new Image();
img.onload = function(){
ele.setSrc(this.src);
}
return {
setSrc : function(src){
img.src = src;
ele.setSrc('loading.png');
}
}
})()
proxy.setSrc('example.png');
複製程式碼
6.快取代理
快取代理是將一些開銷大的運算結果提供暫存功能,當下次計算時,引數和之前一直,則將快取的結果返回:
這個案例參考自JS設計模式-代理模式.
//計算乘積
let mult = function(){
let result = 1;
for(let i = 0; i<arguments.length; i++){
result *= arguments[i];
}
return result;
}
// 快取代理
let proxy = (function(){
let cache = {};
return function(){
let args = Array.prototype.join.call(arguments, '',);
if(args in cache){
return cache[args];
}
return cache[args] = mult.apply(this,arguments);
}
})();
複製程式碼
八、中介者模式(Mediator Pattern)
1.概念介紹
中介者模式(Mediator Pattern) 是用來降低多個物件和類之間的通訊複雜性,促進形成鬆耦合,提高可維護性。
在這種模式下,獨立的物件之間不能直接通訊,而是需要中間物件(mediator
物件),當其中一個物件(colleague
物件)狀態改變後,它會通知mediator
物件,
然後mediator
物件會把該變換通知到任意需要知道此變化的colleague
物件。
2.優缺點和應用場景
2.1優點
- 降低類的複雜度,從一對多轉成一對一。
- 為各個類之間解耦。
- 提高程式碼可維護性。
2.2缺點
中介者會越來越龐大,變得難以維護。
2.3應用場景
- 系統中物件之間存在比較複雜的引用關係,而且難以複用該物件。
- 需要生成最少的子類,實現一箇中間類封裝多個類中的行為的時候。
另外: 不要在職責混亂的時候使用。
3.基本案例
這裡我們實現一個簡單的案例,一場測試結束後,公佈結果,告知解答出題目的人挑戰成功,否則挑戰失敗:
這個案例來自JavaScript 中常見設計模式整理
const player = function(name) {
this.name = name;
playerMiddle.add(name);
}
player.prototype.win = function() {
playerMiddle.win(this.name);
}
player.prototype.lose = function() {
playerMiddle.lose(this.name);
}
const playerMiddle = (function() { // 將就用下這個 demo,這個函式當成中介者
const players = [];
const winArr = [];
const loseArr = [];
return {
add: function(name) {
players.push(name)
},
win: function(name) {
winArr.push(name)
if (winArr.length + loseArr.length === players.length) {
this.show()
}
},
lose: function(name) {
loseArr.push(name)
if (winArr.length + loseArr.length === players.length) {
this.show()
}
},
show: function() {
for (let winner of winArr) {
console.log(winner + '挑戰成功;')
}
for (let loser of loseArr) {
console.log(loser + '挑戰失敗;')
}
},
}
}())
const a = new player('A 選手');
const b = new player('B 選手');
const c = new player('C 選手');
a.win()
b.win()
c.lose()
// A 選手挑戰成功;
// B 選手挑戰成功;
// C 選手挑戰失敗;
複製程式碼
4.書本案例
這個案例來自 《JavaScript 模式》第七章 中介者模式 的案例。
這裡我們有這麼一個遊戲例子,規則是兩個玩家在規定時間內,比比誰點選按鈕次數更多,玩家1按按鍵2,玩家2按按鍵0,並且計分板實時更新。
這裡的中介者需要知道所有其他物件資訊,並且它需要知道哪個玩家點選了一次,隨後通知玩家。玩家進行遊戲的時候,還要通知中介者它做的事情,中介者更新分數並顯示比分。
這裡的player
物件都是通過Player()
建構函式生成,並且都有points
和name
屬性,每次呼叫play()
都會增加1分並通知中介者。
function Player(name){
this.points = 0;
this.name = name;
}
Player.prototype.play = function(){
this.points += 1;
mediator.played();
}
複製程式碼
計分板有個update()
方法,當玩家回合結束就會呼叫,它不知道任何玩家的資訊也沒有儲存分值,只是實現展示當前分數。
let scoreboard = {
// 待更新HTML元素
ele: document.getElementById('result');
// 更新比分
update: function (score){
let msg = '';
for(let k in score){
if(score.hasOwnProperty(k)){
msg = `<p>${k} : ${score[k]}<\/p>`
}
}
this.ele.innerHTML = msg;
}
}
複製程式碼
接下來建立mediator
物件:
let mediator = {
players: {}, // 所有玩家
setup: function(){ // 初始化
let players = this.players;
players.homw = new Player('Home');
players.guest = new Player('Guest');
},
// 當有人玩時 更新分數
played: function(){
let players = this.players
let score = {
Home: players.home.points,
Guest: players.guest.points,
}
scoreboard.update(score);
}
// 處理使用者互動
keypress: function(e){
e = e || window.event; // 相容IE
if(e.which === 49){ // 按鍵1
mediator.players.home.play();
}
if(e.which === 48){ // 按鍵0
mediator.players.guest.play();
}
}
}
複製程式碼
最後就是需要執行和解除安裝遊戲了:
mediator.setup();
window.onkeypress = mediator.keypress;
// 遊戲30秒後結束
setTimeout(function(){
window.onkeypress = null;
alert('遊戲結束');
}, 30000)
複製程式碼
九、觀察者模式(Observer Patterns)
1.概念介紹
觀察者模式(Observer Patterns) 也稱訂閱/釋出(subscriber/publisher)模式,這種模式下,一個物件訂閱定一個物件的特定活動,並在狀態改變後獲得通知。
這裡的訂閱者稱為觀察者,而被觀察者稱為釋出者,當一個事件發生,釋出者會發布通知所有訂閱者,並常常以事件物件形式傳遞訊息。
所有瀏覽器事件(滑鼠懸停,按鍵等事件)都是該模式的例子。
我們還可以這麼理解:這就跟我們訂閱微信公眾號一樣,當公眾號(釋出者)群發一條圖文訊息給所有粉絲(觀察者),然後所有粉絲都會接受到這篇圖文訊息(事件),這篇圖文訊息的內容是釋出者自定義的(自定義事件),粉絲閱讀後可能就會買買買(執行事件)。
2.觀察者模式 VS 釋出訂閱模式
2.1觀察者模式
一種一對多的依賴關係,多個觀察者物件同時監聽一個主題物件。這個主題物件在狀態上發生變化時,會通知所有觀察者物件,使它們能夠自動更新自己。
2.2釋出訂閱模式
釋出訂閱模式理念和觀察者模式相同,但是處理方式上不同。
在釋出訂閱模式中,釋出者和訂閱者不知道對方的存在,他們通過排程中心串聯起來。
訂閱者把自己想訂閱的事件註冊到排程中心,當該事件觸發時候,釋出者釋出該事件到排程中心(並攜帶上下文),由排程中心統一排程訂閱者註冊到排程中心的處理程式碼。
2.3兩者異同點
-
觀察者模式中,觀察者知道釋出者是誰,併發布者保持對觀察者進行記錄。而釋出訂閱模式中,釋出者和訂閱者不知道對方的存在。它們只是通過排程中心進行通訊。
-
釋出訂閱模式中,元件是鬆散耦合的,正好和觀察者模式相反。
-
觀察者模式大多是同步,如當事件觸發,釋出者就會去呼叫觀察者的方法。而釋出訂閱模式大多是非同步的(使用訊息佇列)。
-
觀察者模式需要在單個應用程式地址空間中實現,而釋出-訂閱更像交叉應用模式。
儘管存在差異,但也有人說釋出-訂閱模式是觀察者模式的變異,因為它們概念上相似。
2.4兩者優缺點
相同優點:
- 都可以一對多
- 程式便於擴充套件
不同優點:
- 觀察者模式:單向解耦,釋出者不需要清楚訂閱者何時何地訂閱,只需要維護訂閱佇列,傳送訊息即可
- 釋出訂閱模式:雙向解耦,釋出者和訂閱者都不用清楚對方,全部由訂閱中心做處理
缺點:
- 如果一個被觀察者和多個觀察者的話,會增加維護的難度,並且會消耗很多時間。
- 如果觀察者和釋出者之間有迴圈依賴,可能會導致迴圈呼叫引起系統奔潰。
- 觀察者無法得知觀察的目標物件是如何發生變化,只能知道目標物件發生了變化。
- 釋出訂閱模式,中心任務過重,一旦崩潰,所有訂閱者都會受到影響。
4.基本案例
我們平常一直使用的給DOM節點繫結事件,也是觀察者模式的案例:
document.body.addEventListener('click', function(){
alert('ok');
},false);
document.body.click();
複製程式碼
這裡我們訂閱了document.body
的click
事件,當body
點選它就向訂閱者傳送訊息,就會彈框ok
。我們也可以新增很多的訂閱。
4.觀察者模式 案例
本案例來自 javascript 觀察者模式和釋出訂閱模式。
class Dom {
constructor() {
// 訂閱事件的觀察者
this.events = {}
}
/**
* 新增事件的觀察者
* @param {String} event 訂閱的事件
* @param {Function} callback 回撥函式(觀察者)
*/
addEventListener(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
removeEventListener(event, callback) {
if (!this.events[event]) {
return
}
const callbackList = this.events[event]
const index = callbackList.indexOf(callback)
if (index > -1) {
callbackList.splice(index, 1)
}
}
/**
* 觸發事件
* @param {String} event
*/
fireEvent(event) {
if (!this.events[event]) {
return
}
this.events[event].forEach(callback => {
callback()
})
}
}
const handler = () => {
console.log('fire click')
}
const dom = new Dom()
dom.addEventListener('click', handler)
dom.addEventListener('move', function() {
console.log('fire click2')
})
dom.fireEvent('click')
複製程式碼
5.釋出訂閱模式 案例
本案例來自 javascript 觀察者模式和釋出訂閱模式。
class EventChannel {
constructor() {
// 主題
this.subjects = {}
}
hasSubject(subject) {
return this.subjects[subject] ? true : false
}
/**
* 訂閱的主題
* @param {String} subject 主題
* @param {Function} callback 訂閱者
*/
on(subject, callback) {
if (!this.hasSubject(subject)) {
this.subjects[subject] = []
}
this.subjects[subject].push(callback)
}
/**
* 取消訂閱
*/
off(subject, callback) {
if (!this.hasSubject(subject)) {
return
}
const callbackList = this.subjects[subject]
const index = callbackList.indexOf(callback)
if (index > -1) {
callbackList.splice(index, 1)
}
}
/**
* 釋出主題
* @param {String} subject 主題
* @param {Argument} data 引數
*/
emit(subject, ...data) {
if (!this.hasSubject(subject)) {
return
}
this.subjects[subject].forEach(callback => {
callback(...data)
})
}
}
const channel = new EventChannel()
channel.on('update', function(data) {
console.log(`update value: ${data}`)
})
channel.emit('update', 123)
複製程式碼
參考資料
- 《JavaScript Patterns》
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推薦 | https://github.com/pingan8787/Leo_Reading/issues |
JS小冊 | js.pingan8787.com |
微信公眾號 | 前端自習課 |