單例模式的定義是,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。javascript
有一些對象,好比線程池/全局緩存/瀏覽器中的 window
對象等等,咱們就只須要一個實例。html
下面將根據實際場景進行介紹。java
當咱們單擊登陸按鈕時,頁面中會出現一個登陸的浮窗,而這個登陸浮窗是惟一的,不管單擊多少次登陸按鈕,這個浮窗都只會被建立一次,那麼這個登陸浮窗就適合用單例模式來建立。git
傳統作法在頁面加載完成時,就建立好登陸浮窗,當用戶點擊登陸按鈕時,顯示登陸浮窗,實現代碼以下:github
<button id="loginBtn">登陸</button>
var loginLayer = (() => { let div = document.createElement('div') div.innerHTML = '我是登陸彈窗' div.style.display = 'none' document.body.appendChild(div) return div })() document.getElementById('loginBtn').onclick = () => { loginLayer.style.display = 'block' }
上述代碼有如下缺點:web
DOM
節點,浪費性能。如今優化一下,將代碼改成,在用戶點擊登陸按鈕後,才新增登陸浮窗的 DOM
節點。ajax
代碼以下:算法
var createLoginLayer = () => { let div = document.createElement('div') div.innerHTML = '我是登陸彈窗' div.style.display = 'none' document.body.appendChild(div) return div } document.getElementById('loginBtn').onclick = () => { const loginLayer = createLoginLayer() loginLayer.style.display = 'block' }
上述代碼也存在缺陷,具體以下:設計模式
DOM
節點更加浪費性能。實際上,咱們只須要建立一次登陸浮窗。瀏覽器
經過單例模式,重構上述代碼。
const createLoginLayer = () => { const div = document.createElement('div') div.innerHTML = '我是登陸彈窗' div.style.display = 'none' console.log(123) document.body.appendChild(div) return div } const createSingle = (function () { var instance = {} return function (fn) { if (!instance[fn.name]) { instance[fn.name] = fn.apply(this, arguments) } return instance[fn.name] } })() const createIframe = function () { const iframe = document.createElement('iframe') document.body.appendChild(iframe) iframe.style.display = 'none' return iframe } const createSingleLoginLayer = createSingle(createLoginLayer) const createSingleIframe = createSingle(createIframe) document.getElementById('loginBtn').onclick = () => { const loginLayer = createSingleLoginLayer const iframe = createSingleIframe loginLayer.style.display = 'block' iframe.style.display = 'block' }
通過重構,代碼作了如下優化:
createLoginLayer
/ createIframe
的職責和管理單例對象 createSingle
的職責分離,符合單一職責原則;iframe
/ script
等其餘標籤時,能夠直接複用該邏輯。單例模式是一種簡單但很是實用的模式,特別是惰性單例技術,在合適的時候才建立對象,而且只建立惟一的一個。更奇妙的是,建立對象和管理單例的職責被分佈在兩個不一樣的方法中,這兩個方法組合起來才具備單例模式的威力。
當咱們計劃國慶出去遊玩時,在交通方式上,咱們能夠選擇貴而快的飛機、價格中等但稍慢的動車、便宜但超級慢的火車,根據不一樣的人,選擇對應的交通方式,且能夠隨意更換交通方式,這就是策略模式。
策略模式的定義是,定義一系列算法,把它們一個個封裝起來,而且使它們能夠相互替換。
有一個計算員工年終獎的需求,假設,績效爲 S
的員工年終獎是 4
倍工資,績效爲 A
的員工年終獎是 3
倍工資,績效爲 B
的員工年終獎是 2
倍工資,下面咱們來計算員工的年終獎。
var calculateBonus = function(performanceLevel, salary) { if (performanceLevel === 'S') { return salary * 4; } if (performanceLevel === 'A') { return salary * 3; } if (performanceLevel === 'B') { return salary * 2; } }; calculateBonus('B', 20000); // 輸出:40000 calculateBonus( 'S', 6000 ); // 輸出:24000
上述代碼有如下缺點:
if-else
語句描述邏輯,代碼龐大;S
的獎金係數,必須修改 calculateBonus
函數,違反了開放-封閉原則;使用策略模式改良後
const strategies = { S: salary => { return salary * 4 }, A: salary => { return salary * 3 }, B: salary => { return salary * 2 } } const calculateBonus = (level, salary) => { return strtegies[level](salary) } console.log(calculateBonus('s', 20000)) console.log(calculateBonus('a', 10000))
能夠看到上述代碼作了如下改動:
strategies
封裝了具體的算法和計算過程(每種績效的計算規則);calculateBonus
接受請求,把請求委託給策略類 strategies
(員工的績效和工資;if-else
語句。策略模式使代碼可讀性更高,易於拓展更多的策略算法。當績效係數改變,或者績效等級增長,咱們只須要爲 strategies
調整或新增算法,符合開放-封閉原則。
當網頁上的表單須要校驗輸入框/複選框等等規則時,如何去實現呢?
如今有一個註冊用戶的表單需求,在提交表單以前,須要驗證如下規則:
使用 if-else
語句判斷表單輸入是否符合對應規則,如不符合,提示錯誤緣由。
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <form id='registerForm' action="xxx" method="post"> 用戶名:<input type="text" name="userName"> 密碼:<input type="text" name="password"> 手機號:<input type="text" name="phone"> <button>提交</button> </form> <script type="text/javascript"> let registerForm = document.getElementById('registerForm') registerForm.onsubmit = () => { if (registerForm.userName.value) { alert('用戶名不能爲空') return false } if (registerForm.password.value.length < 6) { alert('密碼長度不能少於6') return false } if (!/(^1[3|5|8][0-9]$)/.test(registerForm.phone.value)) { alert('手機號碼格式不正確') return false } } </script> </body> </html>
上述代碼有如下缺點:
onsubmit
函數龐大,包含大量 if-else
語句;onsubmit
缺少彈性,當有規則須要調整,或者須要新增規則時,須要改動 onsubmit
函數內部,違反開放-封閉原則;使用策略模式重構上述代碼。
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <form action="http://xxx.com/register" id="registerForm" method="post"> 請輸入用戶名: <input type="text" name="userName" /> 請輸入密碼: <input type="text" name="password" /> 請輸入手機號碼: <input type="text" name="phoneNumber" /> <button> 提交 </button> </form> <script type="text/javascript" src="index.js"> </script> </body> </html>
// 表單dom const registerForm = document.getElementById('registerForm') // 表單規則 const rules = { userName: [ { strategy: 'isNonEmpty', errorMsg: '用戶名不能爲空' }, { strategy: 'minLength:10', errorMsg: '用戶名長度不能小於10位' } ], password: [ { strategy: 'minLength:6', errorMsg: '密碼長度不能小於6位' } ], phoneNumber: [ { strategy: 'isMobile', errorMsg: '手機號碼格式不正確' } ] } // 策略類 var strategies = { isNonEmpty: function(value, errorMsg) { if (value === '') { return errorMsg; } }, minLength: function(value, errorMsg, length) { console.log(length) if (value.length < length) { return errorMsg; } }, isMobile: function(value, errorMsg) { if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) { return errorMsg; } } }; // 驗證類 const Validator = function () { this.cache = [] } // 添加驗證方法 Validator.prototype.add = function ({ dom, rules}) { rules.forEach(rule => { const { strategy, errorMsg } = rule console.log(rule) const [ strategyName, strategyCondition ] = strategy.split(':') console.log(strategyName) const { value } = dom this.cache.push(strategies[strategyName].bind(dom, value, errorMsg, strategyCondition)) }) } // 開始驗證 Validator.prototype.start = function () { let errorMsg this.cache.some(cacheItem => { const _errorMsg = cacheItem() if (_errorMsg) { errorMsg = _errorMsg return true } else { return false } }) return errorMsg } // 驗證函數 const validatorFn = () => { const validator = new Validator() console.log(validator.add) Object.keys(rules).forEach(key => { console.log(2222222, rules[key]) validator.add({ dom: registerForm[key], rules: rules[key] }) }) const errorMsg = validator.start() return errorMsg } // 表單提交 registerForm.onsubmit = () => { const errorMsg = validatorFn() if (errorMsg) { alert(errorMsg) return false } return false }
上述代碼經過 strategies
定義規則算法,經過 Validator
定義驗證算法,將規則和算法分離,咱們僅僅經過配置的方式就能夠完成表單的校驗,這些校驗規則也能夠複用在程序的任何地方,還能做爲插件的形式,方便的被移植到其餘項目中。
策略模式是一種經常使用且有效的設計模式,經過上述例子,能夠總結出策略模式的一些優勢:
Context
擁有執行算法的能力,這也是繼承的一種更輕便的代替方案。代理模式是爲一個對象提供一個代用品或佔位符,以便控制對它的訪問。
代理模式的關鍵是,當客戶不方便直接訪問一個對象或者不知足須要的時候,提供一個替身對象來控制對這個對象的訪問,客戶實際上訪問的是替身對象。
傳統作法是小明直接把花送給小白,小白接收到花,代碼以下:
const Flower = function () { return '玫瑰🌹' } const xiaoming = { sendFlower: target => { const flower = new Flower() target.receiveFlower(flower) } } const xiaobai = { receiveFlower: flower => { console.log('收到花', flower) } } xiaoming.sendFlower(xiaobai)
可是,小明並不認識小白,他想要經過小代,幫他打探小白的狀況,在小白心情好的時候送花,這樣成功率更高。代碼以下:
const Flower = function () { return '玫瑰🌹' } const xiaoming = { sendFlower: target => { const flower = new Flower() target.receiveFlower(flower) } } const xiaodai = { receiveFlower: flower => { xiaobai.listenGoodMood().then(() => { xiaobai.receiveFlower(flower) }) } } const xiaobai = { receiveFlower: flower => { console.log('收到花', flower) }, listenGoodMood: fn => { return new Promise((reslove, reject) => { // 10秒後,心情變好 reslove() }) } } xiaoming.sendFlower(xiaodai)
以上,小明經過小代,監聽到小白心情的心情變化,選擇在小白心情好時送花給小白。不只如此,小代還能夠作如下事情:
圖片預加載時一種常見的技術,若是直接給 img 標籤節點設置 src 屬性,因爲圖片過大或網絡不佳,圖片的位置每每有一段時間時空白。
const myImage = (() => { const imgNode = document.createElement('img') document.body.appendChild(imgNode) return { setSrc: src => { imgNode.src = src } } })() myImage.setSrc('https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png')
經過開發者工具把網速設置爲 5kb/s 時,會發如今很長一段時間內,圖片位置是空白的。
下面用虛擬代理優化該功能,把加載圖片的操做交給代理函數完成,在圖片加載時,先用一張loading 圖佔位,當圖片加載成功後,再把它填充進 img 節點。
代碼以下:
const myImage = (() => { const imgNode = document.createElement('img') document.body.appendChild(imgNode) return { setSrc: src => { imgNode.src = src } } })() const loadingSrc = '../../../../img/loading.gif' const imgSrc = 'https://img30.360buyimg.com/ling/jfs/t1/187775/5/8271/435193/60c8117eE7d79ef41/1d21db2c4dca9a90.png' const proxyImage = (function () { const img = new Image() img.onload = () => { myImage.setSrc(img.src) } return { setSrc: src => { myImage.setSrc(loadingSrc) img.src = src } } })() proxyImage.setSrc(imgSrc)
上述代碼有如下優勢:
proxyImage
控制了對 MyImage
的訪問,在 MyImage
未加載成功以前,使用 loading
圖佔位;img
節點設置 src
的函數 MyImage
,預加載圖片的函數 proxyImage
,都只有一個職責;img
節點設置 src
和預加載圖片的功能,被隔離在兩個對象裏,它們能夠各自變化不影響對方。假設咱們要實現一個同步文件的功能,經過複選框,當複選框選中的時候,將該複選框對應的 id 傳給服務器,告訴服務器須要同步 id 對應的文件。
思考一下,會發現,若是每選中一個複選框,就請求一次接口,假設 1s 內選中了 10 個複選框,那麼就要發送 10 次請求。
能夠經過虛擬代理來優化上述作法,新增一個代理,幫助複選框發起同步文件的請求,收集在這 1s 內的請求,1s 後再一塊兒把這些文件 id 發送到服務器。
代碼以下:
<!DOCTYPE html> <html> <meta charset="utf-8" /> <head> <title></title> </head> <body> a <input type="checkbox" value="a" /> b <input type="checkbox" value="b" /> c <input type="checkbox" value="c" /> d <input type="checkbox" value="d" /> <script type="text/javascript" src="index.js"> </script> </body> </html>
const synchronousFile = cache => { console.log('開始同步文件,id爲:'+ cache.join('/')) } const proxySynchronousFile = (() => { const cache = [] let timer return id => { console.log(id) cache.push(id) if (timer) { return } timer = setTimeout(() => { synchronousFile(cache) clearTimeout(timer) timer = null cache.length = 0 }, 2000) } })() const checkbox = document.getElementsByTagName('input') Array.from(checkbox).forEach(i => { console.log(i) i.onclick = () => { if (i.checked) { proxySynchronousFile(i.value) } } })
在列表須要分頁時,同一頁的數據理論上只須要去後臺拉取一次,能夠把這些拉取過的數據緩存下來,下次請求時直接使用緩存數據。
使用緩存代理實現上述功能,代碼以下:
(async function () { function getArticle (currentPage, pageSize) { console.log('getArticle', currentPage, pageSize) // 模擬一個ajax請求 return new Promise((resolve, reject) => { resolve({ ok: true, data: { list: [], total: 10, params: { currentPage, pageSize } } }) }) } const proxyGetArticle = (() => { const caches = [] return async (currentPage, pageSize) => { const cache = Array.prototype.join.call([currentPage, pageSize],',') if (cache in caches) { return caches[cache] } const { data, ok } = await getArticle(currentPage, pageSize) if (ok) { caches[cache] = data } return caches[cache] } })() // 搜索第一頁 await proxyGetArticle(1, 10) // 搜索第二頁 await proxyGetArticle(2, 10) // 再次搜索第一頁 await proxyGetArticle(1, 10) })()
經過緩存代理,在第二次請求第一頁的數據時,直接在緩存數據中拉取,無須再次從服務器請求數據。
上面根據實際場景介紹了虛擬代理和緩存代理的作法。
當咱們不方便直接訪問某個對象時,找一個代理方法幫咱們去訪問該對象,這就是代理模式。
可經過 github源碼 進行實操練習。
但願本文能對你有所幫助,感謝閱讀❤️~
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章。