JS前端中的設計模式和使用場景示例詳解

引言

相信大傢在日常學習和工作中都多多少少聽說/瞭解/使用過 設計模式,我們都知道,使用恰當的設計模式可以優化我們的代碼,那你是否知道對於前端開發哪些 設計模式 是日常工作經常用到或者必須掌握的呢?本文我將帶大傢一起學習下前端常見的設計模式以及它們的 使用場景!!!

本文主講:

  • 策略模式
  • 代理模式

適合人群:

  • 前端人員
  • 設計模式小白/想知道如何在項目中使用設計模式

策略模式

策略模式的定義是:定義一系列的算法,把它們一個個封裝起來,並且使它們可以相互替換。從定義不難看出,策略模式是用來解決那些一個功能有多種方案、根據不同條件輸出不同結果且條件很多的場景,而這些場景在我們工作中也經常遇到,接下來我將用幾個例子來展示策略模式在哪裡用以及如何用。

1.績效考核

假如我們有這麼一個需求,需要根據員工的績效考核給員工發放年終獎(分為A/B/C/D四個等級 分別對應基礎獎金的1/2/3/4倍),我們很容易就寫出這樣的代碼

//level 評級 basicBonus 基礎獎金
const computeBonus(level, basicBonus) = () => {
	if(level === 'A') {
        return basicBonus * 1;
    } else if(level === 'B') {
        return basicBonus * 2;
    } else if(level === 'C') {
        return basicBonus * 3;
    } else if(level === 'D') {
        return basicBonus * 4;
    }
}
computeBonus('A', 1000);//1000

我們發現,以上的代碼可以輕松實現我們的需求,但是這些代碼存在什麼問題呢?

  • computedBonus方法十分臃腫,包含太多if-else
  • 拓展性差,後續如果想要更改評級或者規則都需要進入該函數內部調整。
  • 復用性差。

那策略模式是怎麼解決這些問題的呢?我們都知道,設計模式的核心之一就是將可變的和不可變的部分抽離分裝,那我們根據這個原則來修改我們的代碼,其中可變的就是如何使用這些算法(多少個評級),不變的是算法的內容(評級對應的獎金),下面就是改變後的代碼

//定義策略類
const strategies = {
    'A': function(basicBonus) {
        return basicBonus * 1;
    },
    'B': function(basicBonus) {
        return basicBonus * 2;
    },
    'C': function(basicBonus) {
        return basicBonus * 3;
    },
    'D': function(basicBonus) {
        return basicBonus * 4;
    },
}
//使用策略類
const computeBonus = (level, basicBonus) {
    return strategies[level](basicBonus);
}
computeBouns('A', 1000);//1000

從上面可以看出,我們將每種情況都單獨弄成一個策略,然後根據傳入評級和獎金計算年終獎,這樣我們的computeBonus方法代碼量大大減少,也不用冗雜的if-else分支,同時,如果我們想要改變規則,隻需要在strategies中添加對應的策略,增加瞭代碼的健壯性

2.表單驗證

我們日常的工作中,不可避免地需要做表單相關的業務,畢竟這是前端最初始的職能之一。而表單繞不開表單驗證,那接下來我將帶大傢看看策略模式在表單中如何使用。

需求: 假設我們有一個登錄業務,提交表單前需要做驗證,驗證規則如下:1.用戶名稱不能為空,2.密碼不能少於6位,3.手機格式要正確。

我們很容易寫出以下代碼

const verifyForm = (formData) => {
    if(formData.userName == '') {
        console.log('用戶名不能為空');
        return false
    };
    if(formData.password.length < 6) {
        console.log('密碼長度不能小於6位');
        return false;
    }
    if(( !/(^1[3|5|8][0-9]{9}$)/.test(formData.phone)) {
       console.log('手機格式錯誤');
    	return false
       }
}

顯然,這樣也可以完成表單校驗的功能,但是這樣寫同樣存在著上面說的問題,接下來,我們看下用策略模式如何改寫

//編寫策略對象
const strategies = {
	isEmpty: function(value, error) {
        if(value === '' {
           return error;
           })
    },
    minLength: function(value, len, error) {
        if(value.length < len {
           return error;
           })
    },
    isPhone: function(value, error) {
        if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){ 
 			return errorMsg; 
 			}
    };
}
//接下來我們編寫實現類 用於生成對應的策略實例
class Validator {
    controustor(cache) {
        this.cache = cache || []; //保存校驗規則
    };
    add(dom, rule, error) {
        const arr = rule.splt(':');//分離參數
        this.cache.push(function(){ // 把校驗的步驟用空函數包裝起來,並且放入 cache 
 		const strategy = arr.shift(); // 用戶挑選的 strategy 
 		arr.unshift( dom.value ); // 把 input 的 value 添加進參數列表
 		arr.push( errorMsg ); // 把 error 添加進參數列表
 		return strategies[ strategy ].apply( dom, ary ); 
 	});
    };
    start() {
        for ( let i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){ 
 			var msg = validatorFunc(); // 開始校驗,並取得校驗後的返回信息
 				if ( msg ){ // 如果有確切的返回值,說明校驗沒有通過
 							return msg; 
 							} 
 				}
    }
}
//編寫完策略對象和實例類後我們就可以看看如何使用瞭
const validataFunc = function(){ 
 let validator = new Validator(); // 創建一個 validator 對象
 /***************添加一些校驗規則****************/ 
 validator.add( registerForm.userName, 'isNonEmpty', '用戶名不能為空' ); 
 validator.add( registerForm.password, 'minLength:6', '密碼長度不能少於 6 位' ); 
 validator.add( registerForm.phoneNumber, 'isMobile', '手機號碼格式不正確' ); 
 var errorMsg = validator.start(); // 獲得校驗結果
 return errorMsg; // 返回校驗結果
} 
 var registerForm = document.getElementById( 'registerForm' ); 
 registerForm.onsubmit = function(){ 
 var errorMsg = validataFunc(); // 如果 errorMsg 有確切的返回值,說明未通過校驗
 if ( errorMsg ){ 
 alert ( errorMsg ); 
 return false; // 阻止表單提交
 } 
}; 

這樣,我們就用策略模式將需求改好瞭,之後如果我們的校驗規則改變瞭,修改起來也是很方便的,比如:

validator.add( registerForm.userName, 'isNonEmpty', '用戶名不能為空' ); // 改成:

validator.add( registerForm.userName, 'minLength:10', '用戶名長度不能小於 10 位' );

而且,我們也可以給文本框添加多個校驗規則,隻需要修改下策略對象以及策略方法即可!大大地增強瞭代碼地健壯性。

策略模式的優缺點:

優點:

  • 避免多重條件選擇語句(if-else
  • 具有可拓展性,可獨立抽離封裝,避免重復復制粘貼

缺點:

  • 增加很多策略類或者策略對象,但是這其實不算什麼大缺點
  • 比起直接編寫業務代碼需要思考策略對象以及其他細節

代理模式

代理模式是為一個對象提供一個代用品或占位符,以便控制對它的訪問。代理模式應該是我們日常用到比較多的設計模式瞭(我們日常工作中不知不覺就會用到代理模式,可能你沒發現而已)。

代理模式分為保護代理(用於控制不同權限的對象對目標對象的訪問)和虛擬代理(把開銷很大的對象或者操作延遲到真正需要的時候再去創建 類比引入時動態引入)兩種,但是前端基本不用到保護代理,或者說很難實現保護代理,所以大部分情況下我們用的都是虛擬代理,接下來我主要也是講虛擬代理!

舉個例子,加入A想要給C送情書,但是A沒有直接把情書交給C,而是讓B代為傳送情書,那麼B就是代理,他的職責就是替A做事,這個就是最簡單的代理模式,接下來我們還是老樣子,邊寫需求邊講解

1.圖片懶加載:

相信大傢對於圖片懶加載都不陌生吧,他可以在我們加載出目標圖片前預加載占位圖片,避免空白區域影響體驗,那我們很容易就能寫出下面的代碼

const lazyImage = (function() {
    let imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    let image = new Image;
    image.onload = function() {
        imgNode.src = image.src;//在這裡設置圖片的真正路由
    };
    return {
        setSrc: function(src) {
            imgNode.src = '....'//預加載本地圖片;
            image.src = src
        }
    }
})()
lazyImage.setSrc('https://olddog.jpg');//加載真正的圖片

我們看上面的代碼,也可以完成預加載的功能,但是這樣的代碼存在著什麼樣的問題呢

  • 違反瞭單一職責原則,而且耦合度太高,如果後期我們不需要懶加載瞭,或者需要根據判斷條件判斷是否懶加載,就不得不去動lazyImage的代碼

接下來,我們就用代理模式來改寫一下這個例子

const lazyImage = (function() {
    let imageNode = document.createElement('img');
    document.body.appendChild(imageNode);
    return {
        setSrc: function(src) {
            imageNode.src = src;//設置目標src
        }
    }
})()
//代理函數
const proxyImage = (function() {
    let image = new Image;
    image.onload = function() {
        myImage.setSrc(this.src);
    }
    return {
        setSrc: function(src) {
            myImage.setSrc('....')//預加載本地圖片
            img.src = src
        }
    }
})()
proxyImage.setSrc('https://olddog.jpg');//使用代理加載

我們觀察用代理模式寫的代碼,發現我們將預加載的邏輯轉移到瞭代理函數中,這樣有啥好處呢

  • 如果後期不需要預加載瞭,隻需要取消代理,即將proxyImage.setSrc(...)改成lazyImage.setSrc(...)
  • 代理函數的使用方式和原函數一模一樣,使用者不需要知道代理的實現細節也能使用

不知道大傢有沒有發現,代理函數和原函數有一部分相似的邏輯和操作,隻是代理函數的功能更多,這其實也是代理模式的特征之一,代理函數在保證實現原函數的基本功能的前提下實現更多功能,這樣即使使用者不清楚邏輯也能直接使用,而且後期改動成本很低,隻需要改回原函數的使用即可!!

2.緩存代理

設想一下,如果現在要你寫一個簡單的求積函數,你會怎麼寫

const mult = function() {
	let result = 1;
    for(let i = 0, len = arguments.length; i < len; i++) {
        result *= arguments[i];
    }
    return result
}
mult(1, 2, 3);//6

我們來看一下上面的代碼有啥缺點,上面的代碼雖然實現瞭求積,但是如果我們mult(1,2,3)之後再去mult(1,2,3),那麼系統還是會再計算一遍,這樣無疑是性能浪費,那麼我們就能用代理模式來改寫:

const proxyMult = (function() {
	let cache = {};//緩存計算結果
	return function() {
		const args = Array.prototype.join.call( arguments, ',');
        if(args in cache) {
            return cache[args]
        }
        return cache[args] = mult.apply(this.arguments)
	}
})();
proxyMult(1,2,3);//6
proxyMult(1, 2, 3);//輸出6 但是不會重新計算

可以看到,我們用代理模式改寫後避免瞭重復運算的浪費,這隻是一種情景,還有其他相似情景,比如我們分頁請求數據,可以使用相似的思路,避免對同頁的數據重復請求,這在工作中非常有用!!

總結

我們日常工作中還有很多地方用到代理,比如代理合並請求(間斷性合並而不是全部合並,減少服務器壓力)、惰性加載或創建申請資源等等,而什麼時候使用代理其實不需要提前花很多精力去思考,當我們寫著寫著發現可以抽離使用代理模式的時候再去使用也不遲。由於文章篇幅有限,本文就先講解策略模式和代理模式,後續將繼續更新其他實用的設計模式,喜歡的小夥伴可以點個贊和關註一下,有啥問題可以評論區一起學習交流!

前端設計模式之發佈-訂閱模式

以上就是JS前端中的設計模式和使用場景示例詳解的詳細內容,更多關於前端設計模式場景的資料請關註WalkonNet其它相關文章!

推薦閱讀: