使用合適的設計模式一步步優化前端程式碼

iKcamp發表於2017-10-27

作者:曉飛 本文原創,轉載請註明作者及出處


在後端語言中,設計模式應用的較為廣泛。如Spring中常見的工廠模式、裝飾者模式、單例模式、迭代器模式。但是在日常的前端開發中,設計模式使用的較少,或者大家的程式碼已經遵循了某某設計模式但是我們並不知道。常見的設計模式有23種,如果單純的按照模式名稱+名詞解釋的方式來寫這篇文章,可能太枯燥了或者很難理解記憶,所以我打算換一種方式。下面我們以一個例子開始我們今天的文章。

假設我們有一個這樣的需求:
let page = {
  init: ()=>{
    //此處(placeA)有很多業務程式碼或者呼叫了很多page中的其他初始化函式
  },
  ....
};
複製程式碼

現在業務迭代,需要我們在page.init()初始化程式碼塊的最後增加一些功能,同時不影響原先的功能。按照正常的寫法,我們可能會像下面這樣寫:

let page = {
  init: ()=>{
    //placeA
    page.newFunction();
  },
  newFunction: ()=>{
    ...
  }
};
複製程式碼

這樣寫是可以解決我們的需求,但是這樣的程式碼是具有侵略性的,我們不得不在原先的程式碼的合適位置新增我們需要的程式碼。但我們思考一個問題,如果我們用了某個外掛或者某個被ungly、minify之後的程式碼呢,我們怎麼在找到合適的位置新增我們需要的功能呢?大家可以先自己思考一下,再看下面的內容。

首先我們先看解決方案,再思考其背後的東西。
//我們可以在Function的原型鏈上定義一個擴充套件函式,以實現我們的需求。
Function.prototype.fnAfter = function(fn) {
  var _self = this;
  return function() {
    _self.apply(this, arguments);
    fn.apply(this, arguments);
  }
};

page.init  = (page.init || function() {}).fnAfter(function() {
  console.log('我們要追加的功能成功啦~');
});

page.init();
複製程式碼

上面的程式碼已經能夠實現我們的需要了,但是其實還是不夠好或者可以寫的更靈活一些。因為我希望可以可以做到像jquery的鏈式呼叫那樣,可以一直往後面追加新的功能。那麼我們在上面程式碼的基礎上再擴充套件下,其實很簡單,我們只要再Function.prototype.fnAfter中再返回自身就好了。

Function.prototype.fnAfter = function(fn) {
  var _self = this;
  return function() {
    var fnOrigin = _self.apply(this, arguments);
    fn.apply(this, arguments);
    return fnOrigin;
  }
};
複製程式碼

其實上面的程式碼寫法還是可以優化的。比如:

//每次擴充套件的時候我們都需要這麼寫
page.init  = (page.init || function() {}).fnAfter(function() {
  //...
});
//我們能不能再優化下,比如容錯程式碼 || function(){} 在一個地方統一處理  
//或者我們新建一個工廠函式來幫我們統一做這樣的事情,這裡我們就不展開了,文章篇幅有限。
複製程式碼
我們上面的擴充套件其實就是遵循的是物件導向程式設計中的開放-封閉原則(OCP)。官方對OCP的解釋是:軟體實體(類、模組、函式...)應該是可以擴充套件的,但是不可修改。設計模式中有很多模式都遵循了開發-封閉原則,比如:釋出-訂閱者模式、模板方法模式、策略模式、代理模式。

有的時候我們通過擴充套件來提高程式碼的靈活性並不能解決所有的場景需要,在不可避免發生修改的時候,我們可以通過增加配置檔案,讓使用者修改配置檔案以實現個性化需求也是合理的。修改配置遠比修改原始碼要簡單的多。

有了上面的引入,我們來看幾個前端開發中常見的設計模式。
  • 單例模式

    單例模式顧名思義:保證一個類僅有一個例項,  
    並且對外暴露一個能夠訪問到它的訪問點。
    複製程式碼

    實現單例模式的核心就是保證一個類僅有一個例項,那麼意思就是當建立一個物件時,我們需要判斷下之前有沒有建立過該例項,如果建立過則返回之前建立的例項,否則新建。

    var fn = function() {
      this.instance = null;
    };
    fn.getInstance = function() {
      //寫法1
      if (!this.instance) {
        this.instance = new fn();
      }
      return this.instance;
      
      //寫法2
      return this.instance || (this.instance = new fn());
    };
    
    var fnA = fn.getInstance();
    var fnB = fn.getInstance();
    console.log(fnA === fnB); //true
    複製程式碼

    日常的業務場景中,單例模式也比較常見,比如:一個頁面中的模態框只有一個,每次開啟與關閉的都應該是同一個,而不是重複新建。而且為了效能優化,我們應該在需要時再建立,而不是頁面初始化時就已經存在於dom中,這個就是惰性單例模式

    //假設我們需要點選某個按鈕時就顯示出模態框,那麼我們可以像下面這麼實現。
    var createModal = (function(){
      var modal = null;
      return function() {
        if (!modal) {
          modal = document.createElement('div');
          //...
          modal.style.display = 'none';
          document.getElementById('container').append(modal);
        }
        return modal;
      }
    })();
    
    document.getElementById('showModal').click(function() {
      var modal = createModal();
      modal.style.display = 'block';
    });
    複製程式碼

    上面的程式碼中,我們將建立物件和管理例項的邏輯都放在一個地方,違反了單一職責原則,我們應該單獨新建一個用於建立單例的方法,這樣我們不僅能建立唯一的modal例項,也能建立其他的,職責分開。

    var createSingleInstance = function(fn) {
      var instance = null;
      return function() {
        if (!instance) {
          instance = fn.apply(this, arguments);
        }
        return instance;
      }
    };
    
    var createModal = function() {
      var modal = docuemnt.createElement('div');
      //...
      modal.style.display = 'none';
      document.getElementById('container').append(modal);
      return modal;
    };
    
    var modal = createSingleInstance(createModal);
    複製程式碼

  • 觀察者模式

    定義了物件與其他物件之間的依賴關係,  
    當某個物件發生改變的時候,所有依賴到這個物件的地方都會被通知。
    複製程式碼

    像knockout.js中的ko.compute以及vue中的computed函式其實就是這個模式的實踐。實現觀察者模式的核心就是我們需要有一個變數來儲存所有的依賴,一個listen函式用於向變數中新增依賴,一個trigger函式用於觸發通知。

    var observal = {
      eventObj: {},
      listen: function(key, fn) {
        this.eventObj[key] = this.eventObj[key] || [];
        this.eventObj[key].push(fn);
      },
      trigger: function(key) {
        var eventList = this.eventObj[key];
        if (!eventList || eventList.length < 1) {
          return;
        }
        var length = eventList.length;
        for (var i = 0; i < length; i++) {
          var event = eventList[i];
          event.apply(this, arguments);
        }
      }
    };
    
    //定義要監聽的事件
    observal.listen('command1', function() {
      console.log('黑夜給了我夜色的眼睛~');
    });
    observal.listen('command1', function() {
      console.log('我卻用它尋找光明~');
    });
    observal.listen('command2', function() {
      console.log('一花一世界~');
    });
    observal.listen('command2', function() {
      console.log('一碼一人生~');
    });
    
    //觸發某個監聽的事件
    observal.trigger('command1');//黑夜給了我夜色的眼睛~ 我卻用它尋找光明~
    observal.trigger('command2');//一花一世界~ 一碼一人生~
    複製程式碼

    使用觀察者模式(釋出-訂閱模式)我們可以使得程式碼更靈活、健壯性更高。訂閱者不需要了解訊息來自哪一個釋出者,釋出者也不需要知道訊息會傳送給哪些訂閱者。

    同樣的我們可以建立一個公用的函式庫,裡面存放建立observal的工具方法,需要用到的地方我們就用這個方法建立一個釋出訂閱物件。

  • 其他設計模式及設計原則

    設計模式有很多,這裡篇幅有限就不再展開。GoF在1995年提出了23種設計模式。諸如策略者模式優化表單驗證、代理模式、組合模式、裝飾者模式、介面卡模式...這些後期可以再簡單探討或者大家後面自己瞭解。常用的設計模式及設計原則可以參考下面的思維導圖。

    常用設計模式

    六大設計原則

看了上面的文章,相信大家對設計模式的好處有了直觀的瞭解,也大致掌握了單例模式及觀察者模式。

設計模式都是經過了大量的程式碼、軟體實踐而總結出來的優秀的組織實踐方案。每種設計模式都有它的適應場景,有的場景也會使用多種設計模式。只有瞭解了更多的設計模式,掌握各個設計模式自己的適應場景,才能更好的為我們所用。

但是***過早的優化不一定是好事或者不是必須的***,有時候我們可以一開始並不去優化,等到某個應用場景下出現了程式碼組織混亂、需要額外擴充套件等問題,我們再優化重構,以防過早優化導致的不必要性或者只是增加了程式碼不必要的複雜性。就像redux,如果一個頁面元件與元件之間有資料共享、需要在任意元件內部拿到某個資料、任意一個元件中某個行為導致的資料變化需要通知到所有用到的地方,那麼這個時候可以使用redux,一些簡單的表單頁面或者展示頁完全可以不用redux。

看到這裡不容易,最後給大家講一個笑話輕鬆一下:
從前有隻麋鹿,它在森林裡玩兒,不小心走丟了。  
於是它給它的好朋友長頸鹿打電話:“喂…我迷路辣。”  
長頸鹿聽見了回答說:“喂~我長頸鹿辣~”
複製程式碼

參考:曾探《javascript設計模式與開發實踐》


iKcamp官網:www.ikcamp.com

訪問官網更快閱讀全部免費分享課程:《iKcamp出品|全網最新|微信小程式|基於最新版1.0開發者工具之初中級培訓教程分享》。 包含:文章、視訊、原始碼

使用合適的設計模式一步步優化前端程式碼

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

iKcamp最新活動

使用合適的設計模式一步步優化前端程式碼

報名地址:www.huodongxing.com/event/54099…

“天天練口語”小程式總榜排名第四、教育類排名第一的研發團隊,面對面溝通交流。


使用合適的設計模式一步步優化前端程式碼

2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章