前端設計模式總結

初心,你好嗎發表於2020-11-14

設計模式就是前人總結出來的程式碼模版

建立型

工廠模式

  • 建立物件的工廠,使用者不必關心物件生成的過程,也就是不需要顯示的呼叫new 操作符,只需要呼叫物件工廠暴露出來的建立物件的方法,並傳入需要建立的物件的型別;缺點是擴充套件該工廠需要往工廠裡不斷加入子類,會使程式碼越來越臃腫

抽象工廠模式

  • 在工廠模式的基礎上,有多個工廠,每個工廠負責建立同型別的物件, 抽象工廠實現了獲取每個工廠例項的介面,使用者可以呼叫對應的方法獲取對應型別工廠例項,使用該工廠可以建立物件;缺點和工廠模式一樣,擴充套件麻煩

單例模式

  • 一個類只能被例項化一次,建構函式私有化,在類內部例項化,有多種實現方法

  • /**
    java 建議寫法
    類載入時就初始化,浪費記憶體
    執行緒安全(對java來說)
    **/
    class SingleTon {
      public static getInstance() {return this.instance}
      private instance = new SingleTon()
    	private constructor() {}
    }
    // 使用
    SingleTon.getInstance()
    
  • /**
    js 建議寫法
    使用時再初始化,節約記憶體
    執行緒不安全(對java來說, js單執行緒)
    **/
    class SingleTon {
      public static getInstance() {
        if (!instance) {
          this.instance = new SingleTon();
        }
        return this.instance
      }
      private instance;
    	private constructor() {}
    }
    // 使用
    SingleTon.getInstance()
    
  • /**
    js 閉包版本
    getInstance 返回的函式儲存了對 instance 變數的引用
    **/
    class SingleTon {
      public static getInstance = (function () {
      		let instance = null;
      		return function () {
        	if (!instance) {
            return new SingleTon();
          }
            return instance;
      	}
    	})()
    	private constructor() {}
    }
    // 使用
    SingleTon.getInstance()
    

建造者模式

  • 把簡單物件一步一步組裝成複雜物件
  • 場景:簡單物件固定,簡單物件的組合是變化的

原型模式

  • 快取物件,每次返回物件深拷貝物件(java/C++)

  • (JS)在原型模式下,當我們想要建立一個物件時,會先找到一個物件作為原型,然後通過克隆原型的方式來建立出一個與原型一樣(共享一套資料/方法)的物件

  • 在 JavaScript 裡,Object.create方法就是原型模式的天然實現——準確地說,只要我們還在藉助Prototype來實現物件的建立和原型的繼承,那麼我們就是在應用原型模式。

  • 前端常見的考點就是原型鏈和深拷貝

結構型

介面卡模式

  • 作為兩個不同介面的橋樑
  • 比如Mac Pro 2019 只有四個雷電口,但我想插usb怎麼辦,這就需要一個介面卡來連線兩個介面
  • 不能濫用,適用於要處理介面衝突,但不能重構老程式碼的情況下
  • 把變化留給自己,把統一留給使用者

裝飾器模式

  • 允許向一個現有的物件新增新的功能,同時又不改變其結構

  • 裝飾器和被裝飾的類實現了同一個介面,在裝飾器的建構函式中把類的例項傳入,裝飾器實現該介面時先執行傳入類例項的方法,再執行一系列擴充套件的方法

  • ES7 @語法糖支援裝飾器

  • // 以下是 ts 語法
    interface BaseFunction {
      move: () => void;
    }
    // 比如有個機器人 實現 基本功能 move
    class Robot implements BaseFunction {
      public move() {
        console.log('move');
      }
    }
    
    // 然後有高階需求 機器人需要邊移動邊跳舞並大笑
    // 基於開閉原則 我們不修改原來的 機器人類
    interface AdvanceFunction {
      dance: () => void;
      laugh: () => void;
    }
    class Decorator implements BaseFunction, AdvanceFunction {
      private instance;
      constructor(instance: BaseFunction) {
        this.instance = instance;
      }
      public move() {
        this.instance.move();
        this.dance();
        this.laugh();
      }
      public dance() {
        console.log('dance');
      }
      public laugh() {
        console.log('laugh');
      }
    
    }
    const robot = new Robot();
    robot.move();  // move
    const robotDecorator = new Decorator(robot);
    robotDecorator.move(); // move dance laugh
    // 只要實現了move 的類的例項都可以當作裝飾器建構函式的引數傳入以獲取高階功能
    
  • // ES7 裝飾器寫法
    // ES7 裝飾器 分為類裝飾器,方法裝飾器
    // 給move新增額外的動作,所以我們使用方法裝飾器
    interface BaseFunction {
      move: () => void;
    }
    class Robot implements BaseFunction {
      @decorator // 裝飾move
      public move() {
        console.log('move');
      }
    }
    /**
    @param target 類的原型物件 class.prototype
    @param name 修飾的目標屬性屬性名
    @param descriptor 屬性描述物件
    它是 JavaScript 提供的一個內部資料結構、一個物件,專門用來描述物件的屬性。它由各種各樣的屬性描述符組成,這些描述符又分為資料描述符和存取描述符:
    資料描述符:包括 value(存放屬性值,預設為預設為 undefined)、writable(表示屬性值是否可改變,預設為true)、enumerable(表示屬性是否可列舉,預設為 true)、configurable(屬性是否可配置,預設為true)。
    存取描述符:包括 get 方法(訪問屬性時呼叫的方法,預設為 undefined),set(設定屬性時呼叫的方法,預設為 undefined )
    **/
    function decorator (target, name, descriptor) {
      // 儲存裝飾的方法
      let originalMethod = descriptor.value
      // 在這裡擴充套件裝飾的方法
      descriptor.value = function() {
        dance();
        laugh();
        return originalMethod.apply(this, arguments)
      }
      return descriptor
      function dance() {
        console.log('dance');
      }
      function laugh() {
        console.log('laugh');
      }
    
    }
    const robot = new Robot();
    robot.move();  // dance laugh move
    // 裝飾器函式執行的時候,例項還並不存在。這是因為例項是在我們的程式碼執行時動態生成的,而裝飾器函式則是在編譯階段就執行了。所以說裝飾器函式真正能觸及到的,就只有類這個層面上的物件。
    
    
  • 生產實踐: REACT的高階元件

代理模式

  • 代理就像一箇中介,處理你和target之間的通訊,比如vpn

  • ES6為代理而生的代理器 —— Proxy

    // 第一個引數是我們的目標物件。handler 也是一個物件,用來定義代理的行為。當我們通過 proxy 去訪問目標物件的時候,handler會對我們的行為作一層攔截,我們的每次訪問都需要經過 handler 這個第三方
    const proxy = new Proxy(obj, handler)
    
  • 業務開發中最常見的四種代理型別:事件代理、虛擬代理、快取代理和保護代理

    事件代理

    基於事件的冒泡特性,在子元素上的點選事件會向父級冒泡,所以我們只需要在父元素上繫結一次事件,根據event.target來判斷實際觸發事件的元素,節省了很多繫結事件的開銷

    虛擬代理

    // 常見案例為圖片的預載入
    // 圖片的預載入指,避免使用者網路慢時或者圖片太大時,頁面長時間給使用者留白的尷尬
    // 圖片URL先指向佔點陣圖url, 
    // 在後臺新建一個圖片例項,該圖片例項 的URL指向真實的圖片地址,
    // 當該圖片例項載入完畢時再把頁面圖片的地址指向真實的圖片地址,
    // 這樣頁面圖片就可以直接使用快取展示,
    // 因預載入使用的圖片例項的生命週期全程在後臺從未在渲染層面拋頭露面。
    // 因此這種模式被稱為“虛擬代理”模式
    class LoadImage {
        constructor(imgNode: Element) {
            // 獲取真實的DOM節點
            this.imgNode = imgNode
        }
         
        // 操作img節點的src屬性
        setSrc(imgUrl) {
            this.imgNode.src = imgUrl
        }
    }
    class PreLoadProxy {
        // 佔點陣圖的url地址
        static LOADING_URL = 'xxxxxx'
        private targetImage: LoadImage;
    		constructor(targetImage: LoadImage) {
            this.targetImage = targetImage
        }
        // 該方法主要操作虛擬Image,完成載入
        setSrc(targetUrl): Promise<boolean> {
           // 真實img節點初始化時展示的是一個佔點陣圖
            this.targetImage.setSrc(ProxyImage.LOADING_URL)
            // 建立一個幫我們載入圖片的虛擬Image例項
            const virtualImage = new Image()
            return new Promise((resolve, reject) => {
              // 監聽目標圖片載入的情況,完成時再將DOM上的真實img節點的src屬性設定為目標圖片的url
            virtualImage.onload = () => {
                this.targetImage.setSrc(targetUrl);
              	resolve(true);
            }
            virtualImage.onerror = () => {
              	reject(false);
            }
            // 設定src屬性,虛擬Image例項開始載入圖片
            virtualImage.src = targetUrl;
            });
        }
    }
    const imageList: Element[] = Array.from(document.getElementByClassName('preload-image')));
    Promise.all(imageList.map(image =>  new PreLoadProxy(new LoadImage(image)).setSrc('realUrl')))
    .then()
    .catch()
    

    快取代理

    interface Cal {
      addAll: (...args: number[]) => : number
    }
    // 快取上一次的結果
    // 比如一個累加器
    class Calculator implements Cal {
      addAll(...args: number[]): number {
        if (!args) {
          return 0;
        }
        return args.reduce((pre, next) => pre + next, 0)
      }
    }
    const calculator = new Calculator()
    // 連續執行兩次相同的累加函式會遍歷兩次
    calculator.addAll(1, 2, 3, 5);
    calculator.addAll(1, 2, 3, 5);
    
    class CacheProxy implements Cal {
      private caches: {[key: string]: number} = {}
      private target: Cal;
      constructor(cal: Cal) {
        this.target = cal;
      }
    	addAll(...args: number[]): number {
        const key = args.join();
        if (this.caches[key] === undefined) {
          	this.caches[key] = this.target.addAll();
        }
        return this.caches[key];
      }
    }
    const calculator = new Calculator();
    const calculatorProxy = new CacheProxy(calculator);
    // 連續執行兩次相同的累加函式第二次會使用快取
    calculatorProxy.addAll(1, 2, 3, 5);
    calculatorProxy.addAll(1, 2, 3, 5);
    
    // 寫到這裡,我想這不就是給原來的累加器加了一個快取功能嗎?
    // 加額外的功能又不改變原來的結構這不就符合裝飾器模式的定義嗎
    // 看看是否可以改造成一個快取裝飾器
    // 沒印象的可以看上一小節
    // 裝飾器使用閉包儲存caches物件
    function cacheDecorator(caches = {}) {
      return function decorator(target, name, descriptor) {
        // 儲存裝飾的方法
      	let originalMethod = descriptor.value
      	// 在這裡擴充套件裝飾的方法
      	descriptor.value = function(...args) {
        		const key = args.join();
        		if (caches[key] === undefined) {
          		caches[key] = originalMethod.apply(this, args);
        		}
        		return caches[key];
     	 	}
      	return descriptor
      }
    }
    // 使用快取裝飾器
    class Calculator implements Cal {
      @cacheDecorator()
      addAll(...args: number[]): number {
        if (!args) {
          return 0;
        }
        return args.reduce((pre, next) => pre + next, 0)
      }
    }
    const calculator = new Calculator()
    // 連續執行兩次相同的累加函式同樣會快取
    calculator.addAll(1, 2, 3, 5);
    calculator.addAll(1, 2, 3, 5);
    

    保護代理

    就是ES6的Proxy, 劫持物件的屬性,VUE3.0的雙向繫結實現原理

    行為型

    策略模式

    消滅if else , 定義一系列策略,把它們封裝起來,並使它們可替換

    // 比如你出去旅遊
    // 可以選擇以下交通工具:步行,自行車,火車,飛機
    // 對應需要花的時間
    // 步行  48h
    // 自行車 30h
    // 火車 8h
    // 飛機 1h
    enum Tool {
        WALK = 'walk',
        BIKE = 'bike',
        TRAIN = 'train',
        PLANE = 'plane'
    }
    /**
     * 計算花費的時間
     * if else 一把梭
     * @param tool
     */
    function timeSpend(tool: Tool): number {
        if (tool === Tool.WALK) {
            return 48;
        } else if (tool === Tool.BIKE) {
            return 30;
        } else if (tool === Tool.TRAIN) {
            return 8;
        } else if (tool === Tool.PLANE) {
            return 1;
        } else {
            return NaN
        }
    }
    // 此時新增了一種交通工具 motoBike : 18h
    // 你就必須去改timeSpend函式,在裡面加else if ,
    // 然後你和測試同學說幫忙迴歸一下整套旅遊時間花費邏輯
    // 測試同學嘴上說好的,心裡說了一句草泥馬
    
    // 策略模式重構
    // 把策略抽出來並封裝成一個個函式
    // 使用對映代替if else 
    // 此時新增一種策略,只需要新增一個策略函式並把它放入對映中
    // 這樣你就可以自信的和測試同學說,我增加了一種旅行方式,
    // 你只要測新增的方式,老邏輯不需要回歸
    // 於是你從人人喊打的if else 俠搖身一變成了測試之友
    const timeMap = {
        walk,
        bike,
        train,
        plane
    }
    function timeSpend(tool: Tool): number {
        return timeMap[tool]() || NaN;
    }
    function walk() {
        return 48;
    }
    function bike() {
        return 30;
    }
    function train() {
        return 8;
    }
    function plane() {
        return 1;
    }
    
    
    

    狀態模式

    一個物件有多種狀態,每種狀態做不同的事情,狀態的改變是在狀態內部發生的, 物件不需要清楚狀態的改變,它只用呼叫狀態的方法就行,可以看看這個例子加深理解

    觀察者模式

    當物件間存在一對多關係時,則使用觀察者模式(Observer Pattern)。比如,當一個物件被修改時,則會自動通知依賴它的物件。觀察者模式屬於行為型模式。

    有兩個關鍵角色: 釋出者,訂閱者

    1. 釋出者新增訂閱者
    2. 釋出者發生變化通知訂閱者
    3. 訂閱者執行相關函式
    // 以vue的響應式更新檢視原理為例
    // 資料發生變化,更新檢視
    // observe方法遍歷幷包裝物件屬性
    function observe(target, cb) {
        // 若target是一個物件,則遍歷它
        if(target && typeof target === 'object') {
            Object.keys(target).forEach((key)=> {
                // defineReactive方法會給目標屬性裝上“監聽器”
                defineReactive(target, key, target[key], cb)
            })
        }
    }
    
    // 定義defineReactive方法
    function defineReactive(target, key, val, cb) {
        // 屬性值也可能是object型別,這種情況下需要呼叫observe進行遞迴遍歷
        observe(val)
        // 為當前屬性安裝監聽器
        Object.defineProperty(target, key, {
             // 可列舉
            enumerable: true,
            // 不可配置
            configurable: false, 
            get: function () {
                return val;
            },
            // 監聽器函式
            set: function (value) {
                 // 執行render函式
                render();
                val = value;
            }
        });
    }
    
    class Vue {
        constructor(options) {
            this._data = options.data;
          	//代理傳入的物件,資料發生變化,執行render函式
            observe(this._data, options.render)
        }
    }
    
    let app = new Vue({
        el: '#app',
        data: {
            text: 'text',
            text2: 'text2'
        },
        render(){
            console.log("render");
        }
    })
    

    觀察者模式和釋出-訂閱模式之間的區別,在於是否存在第三方、釋出者能否直接感知訂閱者,

    angular的ngrx, react的redux 和 vue的vuex,event-bus都是典型的釋出訂閱模式

相關文章