Backbone前端框架解讀

2023-02-07 15:02:33

作者: 京東零售 陳震

一、 什麼是Backbone

在前端的發展道路中,前端框架元老之一jQuery對繁瑣的DOM操作進行了封裝,提供了鏈式呼叫、各類選擇器,遮蔽了不同瀏覽器寫法的差異性,但是前端開發過程中依然存在作用域汙染、程式碼複用度低、冗餘度高、資料和事件繫結煩瑣等痛點。

5年後,Backbone橫空出世,通過與Underscore、Require、Handlebar的整合,提供了一個輕量和友好的前端開發解決方案,其諸多設計思想對於後續的現代化前端框架發展起到了舉足輕重的作用,堪稱現代前端框架的基石。

通過對Backbone前端框架的學習,讓我們領略其獨特的設計思想。

二、 核心架構

按照MVC框架的定義,MVC是用來將應用程式分為三個主要邏輯元件的架構模式:模型,檢視和控制器。這些元件被用來處理一個面向應用的特定開發。 MVC是最常用的行業標準的Web開發框架,以建立可延伸的專案之一。 Backbone.js為複雜WEB應用程式提供模型(models)、集合(collections)、檢視(views)的結構。

◦ 其中模型用於繫結鍵值資料,並通過RESRful JSON介面連線到應用程式;

◦ 檢視用於UI介面渲染,可以宣告自定義事件,通過監聽模型和集合的變化執行相應的回撥(如執行渲染)。

如圖所示,當用戶與檢視層產生互動時,控制層監聽變化,負責與資料層進行資料互動,觸發資料Change事件,從而通知檢視層重新渲染,以實現UI介面更新。更進一步,當資料層發生變化時,由Backbone提供了資料層和伺服器資料共用同步的能力。

其設計思想主要包含以下幾點:

◦資料繫結(依賴渲染模板引擎)、事件驅動(依賴Events)

◦檢視元件化,並且元件有了生命週期的概念

◦前端路由設定化,實現頁面區域性重新整理

這些創新的思想,在現代前端框架中進一步得到了繼承和發揚。

三、 部分原始碼解析

Backbone極度輕量,編譯後僅有幾kb,貫穿其中的是大量的設計模式:工廠模式、觀察者模式、迭代器模式、介面卡模式……,程式碼流暢、實現過程比較優雅。按照功能拆分為了Events、Model、Collection、Router、History、View等若干模組,這裡摘取了部分精彩原始碼進行了解析,相信對我們的日常程式碼開發也有一定指導作用:

(1)迭代器

EventsApi起到一個迭代器分流的作用,對多個事件進行解析拆分,設計的非常經典,執行時以下用法都是合法的:

◦用法一:傳入一個名稱和回撥函數的物件

modal.on({
    "change": change_callback,
    "remove": remove_callback
})

◦用法二:使用空格分割的多個事件名稱繫結到同一個回撥函數上

model.on("change remove", common_callback)

實現如下:

var eventsApi = function(iteratee, events, name, callback, opts) {
    var i = 0, names;
    if(name && typeof name === 'object') {
        // 處理第一種用法
        if(callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
        for(names = _.keys(names); i < names.length; i++) events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
    } else if(name && eventSplitter.test(name)) {
        // 處理第二種用法
        for(names = name.split(eventSplitter); i < names.length; i++) events = iteratee(events, names[i], callback, opts);
    } else {
        events = iteratee(events, name, callback, opts);
    }
    return events;
}

(2)監聽器

用於一個物件監聽另外一個物件的事件,例如,在A物件上監聽在B物件上發生的事件,並且執行A的回撥函數:

A.listenTo(B, "b", callback)

實際上這個功能用B物件來監聽也可以實現:

B.on("b", callback, A)

這麼做的好處是,方便對A建立、銷燬邏輯的程式碼聚合,並且對B的侵入程度較小。實現如下:

Events.listenTo = function(obj, name, callback) {
    if(!obj) return this;
    var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
    // 當前物件的所有監聽物件
    var listeningTo = this._listeningTo || (this._listeningTo = {});
    var listening = listeningTo[id];
    
    if(!listening) {
        // 建立自身監聽id
        var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
        listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
    }
    // 執行物件繫結
    internalOn(obj, name, callback, this, listening);
    return this;
}

(3)Model值set

通過option-flags相容賦值、更新、刪除等操作,這麼做的好處是融合公共邏輯,簡化程式碼邏輯和對外暴露api。實現如下:

set: function(key, val, options) {
    if(key == null) return this;
    // 支援兩種賦值方式: 物件或者 key\value
    var attrs;
    if(typeof key === 'object') {
        attrs = key;
        options = val;
    } else {
        (attrs = {})[key] = val;
    }
    options || (options = {});
    ……
    var unset = options.unset;
    var silent = options.silent;
    var changes = [];
    var changing = this._changing; // 處理巢狀set
    this._changing = true;
    
    if(!changing) {
        // 儲存變更前的狀態快照 
        this._previousAttributes = _.clone(this.attributes);
        this.changed = {};
    }
    var current = this.attributes;
    var changed = this.changed;
    var prev = this._previousAttributes;
    
    for(var attr in attrs) {
        val = attrs[attr];
        if(!_.isEqual(current[attr], val)) changes.push(attr);
        // changed只儲存本次變化的key
        if(!_.isEqual(prev[attr], val)) {
            changed[attr] = val;
        } else {
            delete changed[attr]
        }
        unset ? delete current[attr] : (current[attr] = val)
    }
    if(!silent) {
        if(changes.length) this._pending = options;
        for(var i=0; i<changes.length; i++) {
            // 觸發 change:attr 事件
            this.trigger('change:' + changes[i], this, current[changes[i]], options);
        }
    }
    if(changing) return this;
    if(!silent) {
        // 處理遞迴change場景
        while(this._pending) {
            options = this._pending;
            this._pending = false;
            this.trigger('change', this, options);
        }
    }
    this._pending = false;
    this._changing = false;
    return this;
}

四、 不足(對比react、vue)

對比現代前端框架,由於Backbone本身比較輕量,對一些內容細節處理不夠細膩,主要體現在:

◦檢視和資料的互動關係需要自己分類編寫邏輯,需要編寫較多的監聽器

◦監聽器數量較大,需要手動銷燬,維護成本較高

◦檢視樹的二次渲染僅能實現元件整體替換,並非增量更新,存在效能損失

◦路由切換需要自己處理頁面更新邏輯

五、為什麼選擇Backbone

看到這裡,你可能有些疑問,既然Backbone存在這些缺陷,那麼現在學習Backbone還有什麼意義呢?

首先,對於伺服器端開發人員,Backbone底層依賴underscore/lodash、jQuery/Zepto,目前依然有很多基於Jquery和Velocity的專案需要維護,會jQuery就會Backbone,學習成本低;通過Backbone能夠學習用資料去驅動View更新,優化jQuery的寫法;Backbone面對物件程式設計,符合Java開發習慣。

其次,對於前端開發人員,能夠學習其模組化封裝庫類函數,提升程式設計技藝。Backbone的元件化開發,和現代前端框架有很多共通之處,能夠深入理解其演化歷史。