一、什么是設計模式
官方解釋一點就是:模式是一種可復用的解決方案,用於解決軟件設計中遇到的常見問題。
說白了,就是“套路”,舉個例子:我們玩游戲,第一關用了半小時,第二關用了一小時,第三關用了兩小時,......,然后,你花了一個月練到了滿級;
於是你開始練第二個號,這時候呢,其實你已經知道,每一關的捷徑、好的裝備在哪里,所以你按照這個套路,很快的,20 天又練滿了一個號;
身邊有好友問你怎么這么快的又練了一個號,於是你為了造福大眾,你寫了一本 闖關攻略 ~。
通過這個例子,你應該知道,什么是設計模式了吧?烹飪有菜譜,游戲有攻略,干啥都有一些能夠讓我們達到目標的“套路”,在程序世界,編程的“套路”就是設計模式。當然,如果真要給你個定義,我認為,設計模式就是在軟件設計、開發過程中,針對特定問題、場景的更優解決方案。
二、為什么會有設計模式
就還是上邊的例子,魯迅先生說過 : “希望是本無所謂有,無所謂無的。這正如地上的路;其實地上本沒有路,走的人多了,也便成了路”,設計模式是前輩們針對開發中遇到的問題,提出大家公認且有效的解決方案。
1、為什么需要設計模式?
可能有小伙伴確實沒有用過,或者說用了但不知道這就是設計模式。那么為什么我們需要呢?是因為在我們遇到相似的問題、場景時,能快速找到更優的方式解決。
2、如何使用
在 JS 設計模式中,最核心的思想 - 封裝變化。怎么理解,比如我們寫一個東西,這個東西在初始 v1.0
的時候是這樣,到了 v5.0
、v10.0
甚至 v99.0
、v100.0
還是這樣,你愛怎么寫就怎么寫,你只要實現就可以了。
設計模式的核心操作是去觀察你整個邏輯里面的變與不變,然后將變與不變分離,達到使變化的部分靈活、不變的地方穩定的目的。
3、都有啥設計模式 ?
相信了解的,都知道有 20 多種。挺多的,我不扯那么多,其它的我沒用過:什么享元模式、外觀模式、生成器模式啥的,今天就主要聊下前端常用的 6 種設計模式。
三、前端常用的 - 策略模式
1、問題背景
我們先來做一個題,很簡單的,大家肯定都做過 權限邏輯
判斷吧?需求:只用當用戶滿足以下條件,才能看阿寬的這篇文章。給大家 3min
,代碼怎么寫? “ 呵,你這不是看不起老夫嗎?老夫拿起鍵盤,就是 if-else
梭哈,直接帶走,下一個 ! ”
function checkAuth(data) { if (data.role !== 'juejin') { console.log('不是掘金用戶'); return false; } if (data.grade < 1) { console.log('掘金等級小於 1 級'); return false; } if (data.job !== 'FE') { console.log('不是前端開發'); return false; } if (data.type !== 'eat melons') { console.log('不是吃瓜群眾'); return false; } }
相信這段代碼,大家都會寫,那么這么寫,有什么問題 ?
- checkAuth 函數會爆炸
- 策略項無法復用
- 違反開閉原則
聰明的小伙伴已經知道這里要講的是什么模式了,對頭!這里講的就是 策略模式
。那么什么是策略模式呢 ?
2、什么是策略模式
定義 : 要實現某一個功能,有多種方案可以選擇。我們定義策略,把它們一個個封裝起來,並且使它們可以相互轉換。
策略 + 組合 = 絕配
我們用策略模式來改造以下這段邏輯
// 維護權限列表
const jobList = ['FE', 'BE']; // 策略
var strategies = { checkRole: function(value) { return value === 'juejin'; }, checkGrade: function(value) { return value >= 1; }, checkJob: function(value) { return jobList.indexOf(value) > 1; }, checkEatType: function(value) { return value === 'eat melons'; } };
我們已經寫完了策略,接下來要做的就是驗證了
// 校驗規則
var Validator = function() { this.cache = []; // 添加策略事件
this.add = function(value, method) { this.cache.push(function() { return strategies[method](value); }); }; // 檢查
this.check = function() { for (let i = 0; i < this.cache.length; i++) { let valiFn = this.cache[i]; var data = valiFn(); // 開始檢查
if (!data) { return false; } } return true; }; };
此時,小彭同學需要進行權限驗證的條件為:掘金用戶、掘金等級 1 級以上,那么代碼就可以這么寫 :
// 小彭使用策略模式進行操作
var compose1 = function() { var validator = new Validator(); const data1 = { role: 'juejin', grade: 3 }; validator.add(data1.role, 'checkRole'); validator.add(data1.grade, 'checkGrade'); const result = validator.check(); return result; };
然后另一個小伙伴阿寬,他可能需要進行權限驗證的條件為:掘金用戶、前端工程師,那么代碼就可以這么寫:
// 阿寬使用策略模式進行操作
var compose2 = function() { var validator = new Validator(); const data2 = { role: 'juejin', job: 'FE' }; validator.add(data2.role, 'checkRole'); validator.add(data2.job, 'checkJob'); const result = validator.check(); return result; };
這是不是比一直瘋狂寫 if-else
好太多了呢?還有什么例子?最常見的表單驗證啊 ~ 對於表單字段(名稱、密碼、郵箱、....)我們可以使用策略模式去設計優化它。
總結一下:
1、策略規則:先定義各個策略是怎么樣的 —— 策略
2、校驗規則:將所需校驗的策略 add 至緩存起來,需要多少個策略就 add 多少策略 —— 組合
3、校驗規則:最后統一 check ,將所有策略均檢查,得到一個 統一值 即可 —— 統一檢查
3、什么時候用策略模式?
當你負責的模塊,基本滿足以下情況時
- 各判斷條件下的策略相互獨立且可復用
- 策略內部邏輯相對復雜
- 策略需要靈活組合
四、前端常用的 - 發布/訂閱模式
1、問題背景
需求 : 申請成功后,需要觸發對應的訂單、消息、審核模塊對應邏輯。機智如我,我會如何做呢?
function applySuccess() { // 通知消息中心獲取最新內容
MessageCenter.fetch(); // 更新訂單信息
Order.update(); // 通知相關方審核
Checker.alert(); }
不就這樣寫嗎,還想咋滴!!!是的,這么寫沒得毛病,但是呢,我們來思考幾個問題:
比如 MessageCenter.fetch()
是小彭寫的,他哪天把模塊的方法名改了,現在叫 MessageCenter.request()
,你咋辦,你這塊邏輯改唄~
再比如,你和阿寬並行開發的,阿寬負責訂單模塊,你一氣呵成寫下這段代碼,然后一運行,報錯了,一詢問,發現,原來阿寬昨晚去蹦迪了,原本今天應該完成的訂單模塊 Order.update()
,延遲一天,那你就只能先注釋代碼,等依賴的模塊開發完了,你再回來添加這段邏輯咯~
更可怕的是,你可能不只是涉及到這三個模塊,maybe 還有很多模塊,比如你申請成功,現在還需要上報申請日志,你總不能這樣寫吧?
function applySuccess() { // 通知消息中心獲取最新內容
MessageCenter.fetch(); // 更新訂單信息
Order.update(); // 通知相關方審核
Checker.alert(); // maybe 更多
Log.write(); ... }
到這里,我們的 發布-訂閱模式
要按捺不住了。
2、發布 / 訂閱模式
有沒有覺得這個 EventEmitter
好熟悉啊,這不是面試常會問的?發布-訂閱是一種消息范式,消息的發布者,不會將消息直接發送給特定的訂閱者
,而是通過消息通道廣播出去,然后呢,訂閱者通過訂閱獲取到想要的消息。我們用 發布-訂閱模式 修改以下上邊的代碼:
const EventEmit = function() { this.events = {}; this.on = function(name, cb) { if (this.events[name]) { this.events[name].push(cb); } else { this.events[name] = [cb]; } }; this.trigger = function(name, ...arg) { if (this.events[name]) { this.events[name].forEach(eventListener => { eventListener(...arg); }); } }; };
上邊我們寫好了一個 EventEmit
,然后我們的業務代碼可以改成這樣 ~
let event = new EventEmit(); event.trigger('success'); MessageCenter.fetch() { event.on('success', () => { console.log('更新消息中心'); }); } Order.update() { event.on('success', () => { console.log('更新訂單信息'); }); } Checker.alert() { event.on('success', () => { console.log('通知管理員'); }); }
但是這樣就沒問題了嗎?其實還是有弊端的,比如說,過多的使用發布訂閱,就會導致難以維護調用關系。所以,還是看大家的設計吧,這里只是讓大家知道,發布訂閱模式是個啥~
3、什么時候用發布-訂閱模式?
當你負責的模塊,基本滿足以下情況時
- 各模塊相互獨立
- 存在一對多的依賴關系
- 依賴模塊不穩定、依賴關系不穩定
- 各模塊由不同的人員、團隊開發
五、前端常用的 - 裝飾器模式
1、裝飾器模式是什么?
個人理解:是為了給一個函數賦能,增強它的某種能力,它能動態的添加對象的行為,也就是我傳入的就是一個對象。在 JS 世界中,世間萬物,皆為對象。
大家過年,都會買桔子樹,那么我們買了桔子樹之后,都會往上邊掛一些紅包,搖身一變,“紅包桔子樹”,牛掰!這個的紅包就是裝飾器,它不對桔子樹原有的功能產生影響。
再比如 React 中的高階組件 HOC,了解 React 的都知道,高階組件其實就是一個函數,接收一個組件作為參數,然后返回一個新的組件。那么我們現在寫一個高階組件 HOC,用它來裝飾 Target Component
import React from 'react'; const yellowHOC = WrapperComponent => { return class extends React.Component { render() { <div style={{ backgroundColor: 'yellow' }}>
<WrapperComponent {...this.props} />
</div>; } }; }; export default yellowHOC;
定義了一個帶有裝飾黃色背景的高階組件,我們用它來裝飾目標組件
import React from 'react'; import yellowHOC from './yellowHOC'; class TargetComponent extends Reac.Compoment { render() { return <div>66666</div>; } } export default yellowHOC(TargetComponent);
你看,我們這不就用到了裝飾器模式了嘛?什么,你還聽不懂?那我最后再舉一個例子,不知道這個例子,能不能幫助你們理解
const kuanWrite = function() { this.writeChinese = function() { console.log('我只會寫中文'); }; }; // 通過裝飾器給阿寬加上寫英文的能力
const Decorator = function(old) { this.oldWrite = old.writeChinese; this.writeEnglish = function() { console.log('給阿寬賦予寫英文的能力'); }; this.newWrite = function() { this.oldWrite(); this.writeEnglish(); }; }; const oldKuanWrite = new kuanWrite(); const decorator = new Decorator(oldKuanWrite); decorator.newWrite();
六、前端常用的 - 適配器模式
個人理解,為了解決我們不兼容的問題,把一個類的接口換成我們想要的接口。
舉個例子, 我想聽歌的時候,我發現我沒帶耳機,我的手機是 iphone 的,而現在我只有一個 Type-C 的耳機,為了能夠聽歌,我用了一個轉換器(也就是適配器),然后我就可以開心的聽歌了。
再舉個真實業務中的例子,前段時間需要做一個需求,是這樣的。
看這個圖,圖中紅色方框區域是一個資源列表展示組件,該列表數據,有三處來源:本地上傳、資源列表添加、后台返回資源。怎么理解呢?可以看到圖中,該流程主要是:
- 右邊的“資源概況”是調接口,返回的一個 MaterialsList ,可以從右邊點擊 “+” 添加進來
- 也可以通過選擇本地文件上傳
- 如果是編輯場景下,還有后台接口返回的數據
由於歷史原因和之前后台接口返回的數據結構問題,這三個數據格式是不同的。
// 本地資源文件上傳之后的數據結構
export interface ResourceLocalFileType { uuid: string; name: string; size: number; created: number; lastModified: number; resourceType: number; cancel: () => void; status: string; } // 資源概況接口返回的數據結構
export interface ResourcePackageFileType { uuid: string; materialName: string; materialLink: string; materialType: number; uid?: string; ext?: string; } // 原先數據后台返回的數據接口
export interface ResourceBackendFileType { uuid: string; resourceName: string; resourceLink: string; resourceType: number; version: string; ext: string; }
很蛋疼,三個數據來源,三種時候數據結構,我們的資源列表組件是只能接收一種數據格式的列表,我不想破壞純展示型組件的內部邏輯,想保持該組件的職責:展示!那該怎么處理?采用適配器模式,將不同的數據結構適配成展示組件所能接受的數據結構
首先,定義一個統一的數據格式:AdapterResourceFileType
export interface AdapterResourceType { uuid: string; created: number; fileNo: number; fileName: string; fileOrigin: string; fileStatus: string; fileInfo: { type: number; size?: number; [key: string]: any; }; // 本地圖片額外操作
action?: { cancel?: () => void; [key: string]: any; }; }
然后通過適配器模塊,適配成我們需要的接口API。
在數據進行組件列表展示時,將來源不同的數據經過適配器處理,進行整合,然后傳遞給展示組件,以達到我們的目的
適配器就是:為了解決我們不兼容的問題,把一個類的接口換成我們想要的接口,可能不太能幫助理解,我再舉個現實業務中的例子。比如你請求一個接口,接口返回 data = { user: xxx, userName: '' },但是你頁面中用的是 userInfo 和 nickName,不兼容啊,為了解決和這個不兼容問題,我們寫成這樣 return { userInfo: data.user, nickName: data.userName } ,這樣就將請求接口的字段換成我們想要的接口字段了。
簡言之:適配器不是讓你擁有兩種能力,裝飾器才是給一個函數賦能,增強它的某種能力。適配器是:你給我B,但我只能接受A,需要適配器將B轉成我能接受的數據。
七、前端常用的 - 代理模式
1、什么是代理模式
我們再來講一個叫做 代理模式,說到代理哈,我腦海里第一個浮現的詞語 : “事件委托、事件代理”,這算嗎?算噠。我舉些例子,讓大家知道代理模式是個啥玩意。作為程序員嘛,女朋友比較難找,就算找到了,咱這么瘦弱,怕是保護不了啊,所以我花錢找了個保鏢來保護我,穩妥。這就是代理模式。
你翻qiang嗎?你能 google 嗎?老實人哪會什么翻qiang,我是不會的,會我也說我不會。其實正常來講,我們直接訪問 google 是無響應的。那怎么辦呢,通過第三方代理服務器。小飛機?懂 ?
門票都被搶光了,無奈之下,只能找黃牛,這里,黃牛就起了代理的作用,懂?
程序世界的代理者也是如此,我們不直接操作原有對象,而是委托代理者去進行。代理者的作用,就是對我們的請求預先進行處理或轉接給實際對象。
代理模式是為其它對象提供一種代理以控制這個對象的訪問,具體執行的功能還是這個對象本身,就比如說,我們發郵件,通過代理模式,那么代理者可以控制,決定發還是不發,但具體發的執行功能,是外部對象所決定,而不是代理者決定。
// 發郵件,不是qq郵箱的攔截
const emailList = ['qq.com', '163.com', 'gmail.com']; // 代理
const ProxyEmail = function(email) { if (emailList.includes(email)) { // 屏蔽處理
} else { // 轉發,進行發郵件
SendEmail.call(this, email); } }; const SendEmail = function(email) { // 發送郵件
}; // 外部調用代理
ProxyEmail('cvte.com'); ProxyEmail('ojbk.com');
下邊再來舉一個例子,來至 《JavaScript 設計模式與開發實踐》
// 本體
var domImage = (function() { var imgEle = document.createElement('img'); document.body.appendChild(imgEle); return { setSrc: function(src) { imgEle.src = src; } }; })(); // 代理
var proxyImage = (function() { var img = new Image(); img.onload = function() { domImage.setSrc(this.src); // 圖片加載完設置真實圖片src
}; return { setSrc: function(src) { domImage.setSrc('./loading.gif'); // 預先設置圖片src為loading圖
img.src = src; } }; })(); // 外部調用
proxyImage.setSrc('./product.png');
2、代理模式與裝飾器模式區別:
裝飾器模式是給自己添置更高級的裝備,增強自己的屬性;代理模式是和隊友分工合作,把自己不擅長、不想搞的領域交給隊友。
比如說,你很柔弱,你保護不了女朋友,所以你通過健身,擁有一身肌肉。代理模式是,你柔弱,然后花錢請了個有八塊腹肌的保鏢,但是這個保鏢可以決定保護不保護你女朋友。
如何高效學習 —— 學習新的東西的時候,把它擴展成自己比較熟悉的知識,更容易記憶,而且不容易忘。
3、什么時候用代理模式?
當你負責的模塊,基本滿足以下情況時:
- 模塊職責單一且可復用
- 兩個模塊間的交互需要一定限制關系
八、前端常用的 - 職責鏈模式
1、問題背景
需求 :我們申請設備之后,接下來要選擇收貨地址,然后選擇責任人,而且必須是上一個成功,才能執行下一個。小伙伴們驚訝了,這不簡單嘛?
function applyDevice(data) { // 處理巴拉巴拉...
let devices = {}; let nextData = Object.assign({}, data, devices); // 執行選擇收貨地址
selectAddress(nextData); } function selectAddress(data) { // 處理巴拉巴拉...
let address = {}; let nextData = Object.assign({}, data, address); // 執行選擇責任人
selectChecker(nextData); } function selectChecker(data) { // 處理巴拉巴拉...
let checker = {}; let nextData = Object.assign({}, data, checker); // 還有更多
}
你看,這不就完事了,有啥難的,然后過了第二天,你又接了兩個新的流程需求,可能一個就兩步驟,一個可能多了“檢查庫存”這個步驟
你不由驚了,哎呀媽呀,老夫聊發少年狂,鍵盤伺候,Ctrl C + Ctrl V,直接copy然后改一下邏輯??這里就是要講的責任鏈模式。
2、什么是責任鏈模式呢?
我給你們找了個定義 : 避免請求發送者與接收者耦合在一起,讓多個對象都有可能接收請求,將這些對象連接成一條鏈,並且沿着這條鏈傳遞請求,直到有對象處理它為止。
const Chain = function(fn) { this.fn = fn; this.setNext = function() {} this.run = function() {} } const applyDevice = function() {} const chainApplyDevice = new Chain(applyDevice); const selectAddress = function() {} const chainSelectAddress = new Chain(selectAddress); const selectChecker = function() {} const chainSelectChecker = new Chain(selectChecker); // 運用責任鏈模式實現上邊功能
chainApplyDevice.setNext(chainSelectAddress).setNext(chainSelectChecker); chainApplyDevice.run();
這樣的好處是啥?首先是解耦了各節點關系,之前的方式是 A 里邊要寫 B,B 里邊寫 C,但是這里不同了,你可以在 B 里邊啥都不寫。
其次,各節點靈活拆分重組,正如上邊你接的兩個新需求。比如兩個步驟的你就只需要這么寫完事
const applyLincense = function() {} const chainApplyLincense = new Chain(applyLincense); const selectChecker = function() {} const chainSelectChecker = new Chain(selectChecker); // 運用責任鏈模式實現上邊功能
chainApplyLincense.setNext(chainSelectChecker); chainApplyLincense.run();
3、什么時候使用責任鏈模式?
當你負責的模塊,基本滿足以下情況時
- 你負責的是一個完整流程,或你只負責流程中的某個環節
- 各環節可復用
- 各環節有一定的執行順序
- 各環節可重組
補充一下:不是讓大家強行套用設計模式,而是想表達:我們首先需要理解,其次需要形成一種肌肉記憶,正如前邊說的策略模式、發布-訂閱模式的例子一樣,大家在真實開發場景中肯定都有遇到,只是沒有想到,原來這就是設計模式,或者說,原來這里可以用到設計模式去設計。
學習鏈接:https://juejin.cn/post/6844904138707337229