前端設計模式總結

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

創建型

工廠模式

  • 創建對象的工廠,使用者不必關心對象生成的過程,也就是不需要顯示的調用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都是典型的發佈訂閱模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章