Backbone.js

Backbone.js是前端的MVC框架,它經過提供模型Models、集合Collection、視圖Veiew賦予了Web應用程序分層結構。從源碼中能夠知道,Backbone主要分了如下幾個模塊:javascript

(function(root) {
    Backbone.Events     //自定義事件機制
    Backbone.Model       //模型
    Backbone.Collection  //模型集合
    Backbone.Router      //路由配置器
    Backbone.View        //視圖
    Backbone.sync       //向服務器同步數據方法
})(this)

本身主要閱讀了Events、Model、Collection、sync這幾個模塊,因此對這幾個模塊進行介紹。前端

Events模塊

//Backbone的事件對象
var Events = Backbone.Events = {

    //事件訂閱函數
    //name:事件名
    //callback:事件回調函數對象
    //context:事件上下文
    on: function(name, callback, context) {
        //eventsApi的做用請看下方eventsApi方法的註釋
        if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
        //_events對象用於存儲各個事件的回調函數對象列表
        //_events對象中的屬性名爲事件名稱,而屬性值則爲一個保護函數對象的對象數組
        this._events || (this._events = {});
        var events = this._events[name] || (this._events[name] = []);
        //將包含回調函數的對象添加到指定事件的回調函數列表,即註冊事件
        events.push({callback: callback, context: context, ctx: context || this});
        return this;
    },

    //取消事件訂閱
    off: function(name, callback, context) {
        var retain, ev, events, names, i, l, j, k;
        if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
        //當name,callback,context都沒指定時,取消訂閱全部事件
        if (!name && !callback && !context) {
            this._events = void 0;
            return this;
        }

        //未指定name時,則取全部的事件name
        names = name ? [name] : _.keys(this._events);
        //對每一個包含回調函數的對象進行篩選,不符合指定參數條件的進行保留
        for (i = 0, l = names.length; i < l; i++) {
            name = names[i];
            if (events = this._events[name]) {
                this._events[name] = retain = [];
                if (callback || context) {
                    for (j = 0, k = events.length; j < k; j++) {
                        ev = events[j];
                        if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
                            (context && context !== ev.context)) {
                            //保留
                            retain.push(ev);
                        }
                    }
                }
                if (!retain.length) delete this._events[name];
            }
        }

        return this;
    },

    //觸發事件
    //name: 觸發的事件名
    trigger: function(name) {
        if (!this._events) return this;
        //參數列表,不包含name
        var args = slice.call(arguments, 1);
        if (!eventsApi(this, 'trigger', name, args)) return this;
        var events = this._events[name];
        var allEvents = this._events.all;
        //觸發事件,即調用相應的回調函數
        if (events) triggerEvents(events, args);
        //任何事件觸發時,all事件都會被觸發
        if (allEvents) triggerEvents(allEvents, arguments);
        return this;
    }

};

//該函數的主要做用:
//當指定事件名爲object對象時,將object對象中key做爲事件名
//將obejct中的value做爲回調函數對象,而後遞歸調用on、off、trigger
//當指定的事件名爲string,但包含空格時,將string按空格切割,再依次遞歸調用
//該函數須要對應的看它是如何被調用的,直接看是比較難明白的
//當時我就看了很久沒明白,函數名取爲eventsApi對我一點幫助也沒用。。
var eventsApi = function(obj, action, name, rest) {
    if (!name) return true;

    //當指定的事件名爲object時
    if (typeof name === 'object') {
        for (var key in name) {
            obj[action].apply(obj, [key, name[key]].concat(rest));
        }
        return false;
    }

    // 當指定的事件名包含空格時
    //eventSplitter = /\s+/;
    if (eventSplitter.test(name)) {
        var names = name.split(eventSplitter);
        for (var i = 0, l = names.length; i < l; i++) {
            obj[action].apply(obj, [names[i]].concat(rest));
        }
        return false;
    }

    return true;
};

//調用事件回調函數的函數
//多是爲了性能問題,才使用了switch,而不是直接使用default中的代碼
//但我不太明白,這樣爲何會提升效率,但願高人解答
var triggerEvents = function(events, args) {
    var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
    switch (args.length) {
        case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
        case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
        case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
        case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
        default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
    }
};

 

上面的代碼就爲Events的核心部分,咱們能夠從中得知:java

在Events中維護了一個_events對象,而_events對象中的屬性名就表明了一個事件,屬性值爲一個數組,數組中的元素爲包含了注 冊的回調函數的對象。因此當咱們調用on('click', callback, ctx)時,其實是作了這樣的操做:this._events['click'].push({ callback: callback, context: ctx });
Events模塊還提供了once、listenTo、stopListening、listenToOnce等有用的方法,但它們都是基於on、off方法實現的,因此這邊就很少說了。
另外多說幾句:由於javascript的函數也爲對象,是一等公民,因此能夠方便的經過維護函數對象列表來實現事件機制而不是經過設計模式裏的觀察者模式來實現,c#中的事件機制也是如此(基於委託)。

Model模塊

在Backbone中Model是一個構造函數ajax

 

var Model = Backbone.Model = function(attributes, options) {
    var attrs = attributes || {};
    options || (options = {});
    this.cid = _.uniqueId('c');
    //存儲相應model所應具備的屬性
    this.attributes = {};
    if (options.collection) this.collection = options.collection;
    //解析attrs,默認直接返回attrs
    if (options.parse) attrs = this.parse(attrs, options) || {};
    attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
    //設置model相應的屬性
    this.set(attrs, options);
    this.changed = {};
    //用於初始化model的函數,須要使用者本身指定
    this.initialize.apply(this, arguments);
};

隨後咱們能夠看到Model函數的原型擴展,須要注意的是Events對象被拓展到了Model的原型中,這樣model對象也就有了事件機制:sql

//將Events和指定對象擴展至Model的原型中
_.extend(Model.prototype, Events, {
    
    //該方法用於向服務端同步數據(增、刪、改)
    //該方法默認調用的是Backbone.sync方法(ajax)
    //咱們能夠經過替換Backbone.sync來使用咱們本身的sync方法,好比mongodb,這樣backbone也能
    //用於Node.js後端
    sync: function() {
        return Backbone.sync.apply(this, arguments);
    },

    //獲取model的屬性值
    get: function(attr) {
        return this.attributes[attr];
    },

    //設置model的屬性,當屬性值發生變化時,觸發'change'事件
    //該方法爲Model的核心
    set: function(key, val, options) {
        var attr, attrs, unset, changes, silent, changing, prev, current;
        if (key == null) return this;

        //讓set方法能夠這樣調用set({key: value ....}, options);
        if (typeof key === 'object') {
            attrs = key;
            options = val;
        } else {
            (attrs = {})[key] = val;
        }

        options || (options = {});

        //驗證設置的屬性是否符合要求,_validate方法內部將會調用validate方法
        //validate方法須要model使用者本身指定
        //當設置的屬性不符合要求時,直接返回false
        if (!this._validate(attrs, options)) return false;

        //表示應當刪除屬性,而不是設置屬性
        unset           = options.unset;
        //當silent爲true時,不觸發change事件
        silent          = options.silent;
        //變化的屬性列表
        changes         = [];
        //表示是否在變化中
        //這裏我仍是有點疑惑
        changing        = this._changing;
        this._changing  = true;

        if (!changing) {
            //表示變化前的屬性值
            this._previousAttributes = _.clone(this.attributes);
            //存儲改變了的屬性和其屬性值
            this.changed = {};
        }
        current = this.attributes, prev = this._previousAttributes;
      
        if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];

        //設置model屬性值
        for (attr in attrs) {
            val = attrs[attr];
            //當設置的值與當前的model對象的屬性值不一樣時,將要設置的屬性的key加入changes列表中
            if (!_.isEqual(current[attr], val)) changes.push(attr);   
            //this.changed存儲改變了的屬性和其屬性值
            if (!_.isEqual(prev[attr], val)) {
                this.changed[attr] = val;
            } else {
                delete this.changed[attr];
            }
            unset ? delete current[attr] : current[attr] = val;
        }
        
        //觸發change事件
        if (!silent) {
            if (changes.length) this._pending = options;
            //觸發'change:變動的屬性名'事件
            for (var i = 0, l = changes.length; i < l; i++) {
                this.trigger('change:' + changes[i], this, current[changes[i]], options);
            }
        }

        if (changing) return this;
        if (!silent) {
            //觸發'change'事件,這裏使用while,是由於change事件也有可能會調用set方法
            //因此須要遞歸的調用
            while (this._pending) {
                options = this._pending;
                this._pending = false;
                this.trigger('change', this, options);
            }
        }
        this._pending = false;
        this._changing = false;
        return this;
    }
});

 

在這裏我列出來的3個方法:mongodb

首先是sync方法,它用於與服務器端同步,model中的create、update、fetch、destory方法都是經過調用它來跟服務端 交付,backbone默認實現的sync就是經過ajax與服務端交付,因此我認爲,若是咱們將sync替換爲直接與sqlite、mongodb交 付,這樣backbone的Model也能用於服務器端了。json

第二個是get方法:這個方法很簡單,獲取model的屬性值(model的屬性值是存在於model.attributes對象中)c#

第三個是set方法:該方法是model的核心,它用於設置model的屬性值,首先調用this.validate方法(使用者需指定)驗證屬性 值是否符合業務要求,以後對屬性值一一設置,對於改變了的屬性值觸發'change:key'事件(沒有指定silent)。最後再觸發change事 件。後端

Collection模塊

set: function(models, options) {
    //other code.....

    var add = options.add, merge = options.merge, remove = options.remove;
    var order = !sortable && add && remove ? [] : false;

    //迭代參數models,對於其中每一個model進行相應的操做
    for (i = 0, l = models.length; i < l; i++) {
        attrs = models[i] || {};
        if (attrs instanceof Model) {
            id = model = attrs;
        } else {
            id = attrs[targetModel.prototype.idAttribute || 'id'];
        }

        //若是集合中已經存在該對象,則是進行刪除或者修改操做
        if (existing = this.get(id)) {
            //進行刪除操做,記錄下須要刪除的對象
            if (remove) modelMap[existing.cid] = true;
            //進行修改操做
            if (merge) {
                attrs = attrs === model ? model.attributes : attrs;
                if (options.parse) attrs = existing.parse(attrs, options);
                existing.set(attrs, options);
                if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
            }
            models[i] = existing;

            //不然對集合進行添加操做,記錄下應該被添加的對象
        } else if (add) {
            model = models[i] = this._prepareModel(attrs, options);
            if (!model) continue;
            toAdd.push(model);
            this._addReference(model, options);
        }
        if (order) order.push(existing || model);
    }

    //根據以前的記錄下的應刪除的對象,刪除集合中相應的對象
    if (remove) {
        for (i = 0, l = this.length; i < l; ++i) {
            if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
        }
        //刪除集合中相應的對象,並觸發remove事件
        if (toRemove.length) this.remove(toRemove, options);
    }

    //進行添加操做
    if (toAdd.length || (order && order.length)) {
        if (sortable) sort = true;
        this.length += toAdd.length;
        //添加到指定位置,默認添加到末尾
        if (at != null) {
            for (i = 0, l = toAdd.length; i < l; i++) {
                this.models.splice(at + i, 0, toAdd[i]);
            }
         //說實話,我不太明白這段代碼
        } else {
            if (order) this.models.length = 0;
            var orderedModels = order || toAdd;
            for (i = 0, l = orderedModels.length; i < l; i++) {
                this.models.push(orderedModels[i]);
            }
        }
    }

    //當進行了添加或修改操做,而且能夠排序時,則對集合進行排序
    if (sort) this.sort({silent: true});

    if (!options.silent) {
        for (i = 0, l = toAdd.length; i < l; i++) {
            //觸發add事件
            (model = toAdd[i]).trigger('add', model, this, options);
        }
        //觸發排序事件
        if (sort || (order && order.length)) this.trigger('sort', this, options);
    }

    return singular ? models[0] : models;
}

 

我列出了set方法中的大部分代碼,它根據指定的參數進行添加 刪除 修改操做,並進行排序。而我我的感受這樣不是太好,由於一個方法作了太多的事情,有點array.splice的味道,使得整個方法的代碼十分冗長,也變得不易理解。。。我比較菜,看這個看了很久。。設計模式

Collection雖然提供了set,但它仍是提供了add(內部調用set)、reset(內部調用set)、remove方法。Collection還跟Model同樣提供了sync方法,用於和服務端同步數據。

最後,Collection還提供了underscore.js庫對集合的操做方法,它們都是調用underscore庫實現的方法。

Sync模塊

sync是Backbone用於同步服務端數據的方法,它的默認實現:

 

Backbone.sync = function(method, model, options) {
    var type = methodMap[method];

    //some init code....

    //默認使用json格式
    var params = {type: type, dataType: 'json'};

    if (!options.url) {
        params.url = _.result(model, 'url') || urlError();
    }

    //將請求的數據類型設置爲json
    //將對象格式化爲json數據
    if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
        params.contentType = 'application/json';
        params.data = JSON.stringify(options.attrs || model.toJSON(options));
    }

    //some for old server code....
    //and some for ie8 code


    // ajax請求
    var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
    model.trigger('request', model, xhr, options);
    return xhr;
}

 

上面就是它的主要代碼,我將一部分有關兼容性的代碼給移除了。

 

閱讀源碼的收穫:

由於本身接觸js不久,就想去看看一個優秀的js項目是如何寫的,因此就選擇了backbone這個相對比較輕量級的框架。固然,由於本身水平有 限,加上寫的js代碼也很少,不能很好領悟backbone的設計思想,也不能很好的指出backbone有什麼不足的地方,但我仍是有一些收穫:

1.學到了js中的一些使用技巧,好比使用||操做符 model || model = {},還有如何利用參數在代碼中實現相似重載的行爲(js函數自己沒有重載)

2.對this變量的綁定有了更好的理解

3.相對於c#而言,js是一門弱類型的動態語言,因此對一個對象的擴展要靈活多

4.在c#中,若是我須要去提升模塊的可擴展性,我可能要利用接口利用多態去實現,但js則就輕鬆的多,我只需暴露一個屬性接口便可,由於我能夠輕 鬆的替換他,就像Backbone.sync同樣,但帶來的缺點就是若是你的sync方法並不符合設計,你只會在運行時發現錯誤,而不是編譯時

相關文章
相關標籤/搜索