博客 / 詳情

返回

web前端mvc庫實現

web前端mvc庫實現

前言

隨着前端應用日趨複雜,如今如angular,vue的mvvm框架,基於virtual dom的react等前端庫基本成為了各個公司的首選。而以當初最流行的頭號大哥backbone為代表的mvc庫基本退出了歷史舞台。

在現如今人人都説mvvm/react多好,backbone多差的時代。筆者看別人文章,看的時候總是感覺好像有點道理,看完之後如耳邊風一般左耳朵進,右耳朵出。

so,痛定思痛之後,筆者定了個小目標,實現了份簡易版的backbone庫。以設計,實現的角度來對比其它類型庫的差異。

ok,廢話不多説,上正菜。

思路整理

MVC即將前端應用抽象為Model,View,Control三大模塊。View為用户視圖,通過瀏覽器事件接受用户輸入。Model為數據模型,他可以隨時和後端同步數據。Control則是具體實現View派發的事件,計算並改變Model的數據。

UI可以被抽象為模版+數據,隨着用户不斷的觸發瀏覽器提供的各種事件,交互不斷的進行,Control接受了View指令改變着Model的數據,而View則隨着Model的改變做出響應,最終展現在用户面前。

流程圖:

圖片描述

模塊劃分

本篇文章的思路來自於backbone,並屏棄了耦合的後端操作。早期MVC並沒有對Control做嚴格的劃分,也許是數據的改變計算並不那麼複雜,所以Control功能在View的事件內完成了,也就是説View模塊裏面耦合了Control的功能。

但近幾年flux的action,store的出現,View調用action,具體數據變化計算則在store內部實現,也算是把Control功能從View內部抽象出來了吧。

Event模塊

為對象提供對事件的處理和回調,內部實現了觀察者(訂閲者)模式,如view訂閲了model的變化,model變化之後則通知view。

基本方法。
  • on函數通過event名,在object上綁定callback函數,將回調函數存儲在數組裏。

  • off函數移除在object上綁定的callback函數

    • 通過event名移除指定callback。如object.off("change", onChange)

    • 通過event名移除所有callback。如object.off("change")

    • 移除所有指定callback。如object.off(null, onChange);

    • 移除所有指定context的callback。如object.off(null, null, context);

    • 清空object所有的callback。如object.off()

  • trigger函數通過event名,找到object對應的數組,並觸發所有數組內回調函數。

注意事項

其所有方法應該支持類似on(name,callback),on('name1 name2 name3',callback), on({name1:callback1,name2:callback2})

這時候則可以抽象內部公用方法。通過遞歸的方式,on({name1:callback1,name2:callback2})類型的和on('name1 name2 name3',callback)類型,最終轉化為最基本的on(name,callback)類型。核心代碼如下:

this.eventsApi = function (iteratee, name, callback, context) {
    let event;
    if (name && typeof name === 'object') {
      Object.keys(name).forEach(key=> {
        event = this.eventsApi(key, name[key], context);
      })
    } else if (SEPARATE.test(name)) {
      var keys = name.split(SEPARATE);
      keys.forEach(key=> {
        event = iteratee.call(this,key, name[key], context);
      });
    } else {
      event = iteratee.call(this,name, callback, context);
    }
    return event;
};

View模塊

  • 無狀態,實例化的時候可以對應多個model實例,並以觀察者的身份觀察這些model的變化,通過這些model數據,加上指定的模版渲染dom,展示UI。

  • 銷燬的時候註銷掉所有model的觀察,取消與相關model之間的關聯。

  • 實例化的時候通過事件委託註冊瀏覽器事件

實現
    • _ensureElement,確保View有一層dom包裹,如果this.el這個dom不存在,則通過id,className,tagName創建一個dom並賦值於this.el。

    • listenTo,將model與view實例關聯起來,並收集關聯model,存儲於listenTo數組內,內部實現則是調用model的on函數

    • stopListening,view銷燬前調用,通過listenTo數組找到關聯model,並取消view與這些model之間的觀察者關係。

    • $,將dom的查找定位在 this.$el下

    • delegateEvents,事件委託,以{'click #toggle-all': 'choose'}為例,為在this.el子節點的id等於toggle-all的dom註冊click事件choose函數。核心代碼如下:

    delegateEvents: function (events) {

       var $el = this.$el;
       Object.keys(events).forEach(item=> {
         var arr = item.split(' ');
         if (arr.length === 2) {
           var event = arr[0];
           var dom = arr[1];
           $el.on(event + '.delegateEvents' + this.$id, dom, this[events[item]].bind(this));
         }
       })
     },
    
    • undelegateEvents,註銷掉通過delegateEvents註冊的dom事件

    Model模塊

    Model在backbone裏被抽象為object類型的Model和array類型的Collection

    • 承載着應用的狀態,可以隨時和後端保持同步。

    • 內部實現了對數據變化的監聽,一旦發生變化則通知觀察者View發生變化。

    Model

    監聽數據的變化,對model的修改,刪除之後調用對應的trigger函數,通知訂閲了model變化的view。

    • set函數,改變model數據,並觸發change事件

    set: function (obj) {
        this._changing = true;
        this.changed = obj;
        this._previousAttributes = Object.assign({}, this.attributes);
        this.attributes = Object.assign({}, this.attributes, obj);
        const keys = [];
        Object.keys(obj).forEach(key=> {
          keys.push(key);
          this.trigger('change:' + key, this);
        }, this);
    
        if (keys.length > 0) {
          this.trigger('change', this);
        }
        this._changing = false;
     },
    
    • destroy函數觸發destroy事件

    destroy: function () {
       this.stopListening();
       this.trigger('destroy', this);
    },
    
    Collection

    提供數組類型models的push,unshift,pop,shift,remove,reset等功能。push,unshift實際調用add函數,pop,shift實際調用remove函數。

    • add函數支持任意索引插入指定數組,觸發add事件。核心的代碼如下:

        export const splice = (array, insert, at)=> {
          at = Math.max(0, Math.min(array.length, at));
          let len = insert.length;
          let tail = [];
          for (let i = at; i < array.length; i++) {
            tail.push(array[i]);
          }
          for (let i = 0; i < tail.length; i++) {
            array[i + at + len] = tail[i];
          }
          for (let i = 0; i < len; i++) {
            array[i + at] = insert[i];
          }
          return array;
        };
    
    • remove函數支持刪除指定model,觸發update事件。

    • _addReference,調用add方法新增model時,通過觀察者模式增加該model與collection之間的關聯,model的變化通知collection。核心代碼如下:

        _addReference: function (model) {
          model.on('all',this._onModelEvent,this);
         }
    • _removeReference,調用remove,reset移除model時,取消該model與collection關聯。核心代碼如下:

        _removeReference: function(model) {
           if (this === model.collection) delete model.collection;
           model.off('all', this._onModelEvent, this);
        }
    

    extend

    生產環境下需要在保留原生View,Model類的功能情況下做一些業務拓展,這時候需要用到類的繼承。

    雖然es6支持extend繼承,但這邊我還是手寫了一份。思路則是返回一個構造函數,該函數的原型為新的實例對象props,而props的原型對象則是父函數的原型(有點拗口,自己看代碼理解)。
    核心代碼如下:

    export const extend = function (props) {
      var parent = this;
      var child = function () {
        parent.apply(this, arguments);
      };
      child.prototype = Object.assign(Object.create(parent.prototype), props, { constructor: child });
      return child;
    };

    todomvc效果圖

    圖片描述

    源碼

    web前端mvc實現

    小節

    整篇文章基本是圍繞着如下2點

    • view-model,collection-model的觀察者模式的實現展開,期間view,model的銷燬則取消與之有關聯對象的關係,如view銷燬時,註銷掉與之關聯的model的回調函數。

    • 監聽數據變化,並通知觀察者作出響應,如model變化後觸發trigger('change')

    好了,文章草草寫到這了,多謝各位看官,以上也是純個人觀點,有問題歡迎各位web前端mvc設計指教。

    user avatar
    0 位用戶收藏了這個故事!

    發佈 評論

    Some HTML is okay.