Backbone.Events—純凈MVC框架的雙向綁定基石


Backbone.Events—純凈MVC框架的雙向綁定基石

為什么Backbone是純凈MVC?

在這個大前端時代,各路MV*框架如雨后春筍搬涌現出來,在infoQ上有一篇
12種JavaScript MVC框架之比較,勝出的是Ember.js,當然這只是
Gordon L. Hempton的一家之言(Ember.js確實有其強大之處),關於孰強孰弱,大家肯定有自己心中的No.1。關於到底有多少種前端MVC
框架,愚安我肯定是不知道的,除了上面提到的12種以外,還有很多國內國外的MV*框架,大家造輪子的熱情也無比高漲,各種demo活躍在
各大技術社區。一時間有句調侃的話——前端MV*哪家強,不服寫個TodoList,這里有一個目前主流的MV*框架寫的Todolist的example,叫做
Helping you select an MV* framework大家可以稍作了解。

我們知道,MV*框架的優勢在於,在結構上其可以組織良好的結構化、模塊化代碼;在邏輯上,實現以下功能:

  • 構建DOM
  • 實現視圖邏輯
  • 在模型與視圖間進行同步
  • 管理復雜的UI交互操作
  • 管理狀態和路由
  • 創建與連接組件

在諸多的此類框架中,筆者真正在生產環節使用過的聊聊無幾,如,Angular,Backbone,Ember,React,其余的我就不敢多言了。
其中,React.js使用的是一種叫做virtual dom的概念,讓我眼前一亮。Angular.js采用一種預編譯技術,將dom中的元素與Controler的scope
結合起來,然后采取臟輪訓的方式監聽二者的變化,實現模型數據與dom間的雙向綁定,實時更新。而Ember.js作為Ruby on Rails框架開發團隊的
又一力作,其野心可以從其類庫的強大看出,單純的Ember.js文件就有足足141kb,而且Ember在視圖層提供了數據綁定的功能,可以輕松實現
頁面數據與模型的數據綁定。

而今天愚安要說的Backbone.js相比以上三者,就顯的弱小多了,其文件大小只有18kb(無依賴未壓縮)。為了單純的實現一個MVC結構,Backbone
並沒有像其他框架那樣,花大力氣增強自己的工具類庫。其在操作dom和ajax上完全依賴jQuery,在工具類上完全依賴underscore

正是因為如此,Backbone的結構十分簡潔清晰,易於擴展,所以Backbone得開源社區十分活躍,插件數量在所有MV*框架中鶴立雞群。所以,加上注釋也只有1700
余行的Backbone是一個純凈的MVC框架。

Backbone的結構

打開Backbone的官網,我們發現構成Backbone的模塊只有Events,Model,Collection,Router,History,Sync,
View,noConflict幾部分組成。

折疊后的Backbone關鍵代碼如下:

Backbone.VERSION = '1.1.2';//版本
Backbone.$ = $;
//出讓對Backbone命名空間的所有權
Backbone.noConflict = function() {
};
//Events事件
var Events = Backbone.Events = {
};
_.extend(Backbone, Events);
//Model模型
var Model = Backbone.Model = function(attributes, options) {
};
_.extend(Model.prototype, Events, {
});
//Collection集合
var Collection = Backbone.Collection = function(models, options) {
};
_.extend(Collection.prototype, Events, {
});
//View視圖
var View = Backbone.View = function(options) {
};
_.extend(View.prototype, Events, {
});
//sync同步方法
Backbone.sync = function(method, model, options) {
};
//貼出只是為了佐證Backbone的ajax是使用jQuery的ajax,而不是像Angular.js那樣實現自己的$http
Backbone.ajax = function() {
    return Backbone.$.ajax.apply(Backbone.$, arguments);
};
//Router路由
var Router = Backbone.Router = function(options) {
};
_.extend(Router.prototype, Events, {    
});
//History瀏覽歷史(window.history)
var History = Backbone.History = function() {
};
_.extend(History.prototype, Events, {
});
Backbone.history = new History;
//在underscore基礎上實現的關鍵性的繼承方法,這個也很關鍵
var extend = function(protoProps, staticProps) {
});
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;

不得不承認Backbone的代碼真的非常簡潔清晰。

Events如何實現從Model到View的綁定?

通過上面的簡單折疊代碼,我們可以看出,不管是Model,Collection,Router,History,History,甚至是Backbone本身,
都或通過原型鏈或直接繼承了Backboe.Events。這也是在Backbone的代碼編寫順序上Backboe.Events會放在最前面的原因。

那么Backboe.Events到底做了些什么呢?還是貼代碼最有說服力:

var Events = Backbone.Events = {
    //綁定一個事件到`callback`回調函數上。通過 `"all"`可以綁定這個回調函數到所有事件上
    on: function(name, callback, context) {
    },
    //綁定一個僅會被觸發一次的事件。在這個事件的回調函數被調用一次之后,這個回調函數將被移除
    once: function(name, callback, context) {
    },
    //移除一個或多個的事件回調
    off: function(name, callback, context) {
    },
    //觸發一個或多個事件,調用對應的回調函數。
    trigger: function(name) {
    },
    //`on`和`once`的控制反轉版本。告訴當前對象去監聽另一個對象的事件
    listenTo: function(obj, name, callback) {
    },
    listenToOnce: function(obj, name, callback) {
    },
    //告訴當前對象停止對指定對象的指定事件的監聽,或停止所有監聽
    stopListening: function(obj, name, callback) {
    }
  };

基於這樣的一個Events對象的實現,Backbone可以輕松實現了很多功能,如在Model.set(key,value)時觸發一個change事件,視圖層在撲捉到
這個事件的時候,對dom做出相應的更新,這樣就實現了Model層到View層的綁定。例如:

var View = Backbone.View.extend({
    initialize:function(){
        this.listenTo(this.model,'change:name',this.onNameChange);
    },
    onNameChange:function(){
        this.$('.name').text(this.model.get('name'));
    }
    template: '<span class="name"></span>'
});
var m = new Backbone.Model({name:'Jack'});
var v = new View({model:m});
m.set('name','John');

那么,這里的set為什么會觸發change事件呢?具體實現我還是貼一下源碼和自己的中文注釋:

Backbone.Model.prototype.set = function (key, val, options) {
      var attr, attrs, unset, changes, silent, changing, prev, current;
      if (key == null) return this;
      //格式化參數
      if (typeof key === 'object') {
        attrs = key;
        options = val;
      } else {
        (attrs = {})[key] = val;
      }
      options || (options = {});
      //執行當前對象的驗證方法
      if (!this._validate(attrs, options)) return false;
      //提取屬性和可選項
      unset           = options.unset;
      silent          = options.silent;
      changes         = [];
      changing        = this._changing;
      this._changing  = true;
      //標記當前Model是否改變,並記錄改變的屬性及其變化前后的值
      if (!changing) {
        this._previousAttributes = _.clone(this.attributes);
        this.changed = {};
      }
      current = this.attributes, prev = this._previousAttributes;
      //若改變的屬性為id,則同時改變當前對象的id
      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
      //遍歷`set`的屬性,更新或刪除對應屬性的當前值
      for (attr in attrs) {
        val = attrs[attr];
        if (!_.isEqual(current[attr], val)) changes.push(attr);
        if (!_.isEqual(prev[attr], val)) {
          this.changed[attr] = val;
        } else {
          delete this.changed[attr];
        }
        unset ? delete current[attr] : current[attr] = val;
      }
      //若非沉默更新(傳參時options.silent=true),觸發change:attr事件
      //attr為各個對應被set的屬性的key,並傳當前值到回調函數
      if (!silent) {
        if (changes.length) this._pending = options;
        for (var i = 0, l = changes.length; i < l; i++) {
          this.trigger('change:' + changes[i], this, current[changes[i]], options);
        }
      }
      //change可以遞歸嵌套到change事件中
      if (changing) return this;
      if (!silent) {
        while (this._pending) {
          options = this._pending;
          this._pending = false;
          this.trigger('change', this, options);
        }
      }
      this._pending = false;
      this._changing = false;
      return this;
    }

所以若想,監聽到Model的屬性變化,改變Model的屬性值時,必須采用Model.set()方法,而不能簡單的使用Model.attributes[key] = value
其實這個是一個兼容的做法,我們知道Backbone對低版本瀏覽器的支持非常好,如果不考慮這些的話,完全可以使用更高級的API

Object.observe(this.attributes, function(changes){
    if (!silent) {
        if (changes.length) this._pending = options;
        for (var i = 0, l = changes.length; i < l; i++) {
          this.trigger('change:' + changes[i].name, this, current[changes[i].name], options);
    }
}.bind(this));

當然,這只是愚安我的一點意淫,沒有什么實際意義。

實際上,Backbone內部事件除了change以外還有很多,這里簡單列舉一下:

  • "add" (model, collection, options) — 當一個model被add到一個collection時
  • "remove" (model, collection, options) — 當一個model從一個collection移除時
  • "reset" (collection, options) — 當一個collection的實體內容已經被替換掉時
  • "sort" (collection, options) — 當一個collection的內容被重新排序時
  • "change" (model, options) — 當一個model的屬性被改變時
  • "change:[attribute]" (model, value, options) — 當一個model的指定屬性被改變時
  • "destroy" (model, collection, options) — 當一個model被銷毀時
  • "request" (model_or_collection, xhr, options) — 當一個model或collection開始發起一個向服務端的請求時
  • "sync" (model_or_collection, resp, options) — 當一個model或collection已經成功與服務端同步時
  • "error" (model_or_collection, resp, options) — 當model或collection對服務端的請求已經失敗時
  • "invalid" (model, error, options) — 當一個model的驗證失敗時
  • "route:[name]" (params) — 當路由的一個指定path被匹配時由路由器觸發
  • "route" (route, params) — 當任意路由被匹配時由路由器觸發
  • "route" (router, route, params) — 當任意路由被匹配時由history觸發
  • "all" — 任意事件,事件名稱作為第一個參數傳遞

Events如何實現從View到Model的綁定?

上面我們已經知道基於強大的Backbone.Events,我們可以輕松的實現model到view的綁定,反之呢?

Backbone沒有類似Angular.js的預編譯機制,也沒有View-Model的概念,從View到Model的綁定依賴於原生DOM事件的監聽,完整雙向綁定如:

var View = Backbone.View.extend({
    initialize:function(){
        this.listenTo(this.model,'change:name',this.onNameChange);
    },
    onNameChange:function(){
        this.$('.name').text(this.model.get('name'));
    }
    template: '<input type="text" class="name-input"><span class="name"></span>',
    events: {'input .name-input': '_changeName'},
    _changeName: function(e){
        var value = e.currentTarget.value;
        this.model.set('name', value);
        return false;
    }       
});
var m = new Backbone.Model({name:'Jack'});
var v = new View({model:m});
m.set('name','John');

需要注意的是View層的events字典,其實就是DOM事件,而不是Backbone.Events。而且,純凈的Backbone這里用的是jQuery的jQueryon 方法
進行綁定的。

好的,愚安又貼了很多源碼,和一些自己對文檔的不成翻譯,有沒有干貨,見仁見智了。另外,本人是非常推薦剛接觸前端框架的童鞋,以Backbone
做為開始的。就像學習PHP的MVC框架,我非常推薦以codeigniter作為開始的,沒有強大的封裝,但有着最基本純凈的MVC思想。

注:本文是以Backbone的1.1.2版本為基礎的

引用:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM