1. 程式人生 > >淺析前端開發中的 MVC/MVP/MVVM 模式

淺析前端開發中的 MVC/MVP/MVVM 模式

所有 團隊 sub 策略 代碼 告訴 簡單 ava 關心

MVC,MVP和MVVM都是常見的軟件架構設計模式(Architectural Pattern),它通過分離關註點來改進代碼的組織方式。不同於設計模式(Design Pattern),只是為了解決一類問題而總結出的抽象方法,一種架構模式往往使用了多種設計模式。

要了解MVC、MVP和MVVM,就要知道它們的相同點和不同點。不同部分是C(Controller)、P(Presenter)、VM(View-Model),而相同的部分則是MV(Model-View)。

Model&View

這裏有一個可以對數值進行加減操作的組件:上面顯示數值,兩個按鈕可以對數值進行加減操作,操作後的數值會更新顯示。

技術分享

我們將依照這個“栗子”,嘗試用JavaScript實現簡單的具有MVC/MVP/MVVM模式的Web應用。

Model

Model層用於封裝和應用程序的業務邏輯相關的數據以及對數據的處理方法。這裏我們把需要用到的數值變量封裝在Model中,並定義了add、sub、getVal三種操作數值方法。

var myapp = {}; // 創建這個應用對象

myapp.Model = function() {
    var val = 0; // 需要操作的數據

    /* 操作數據的方法 */
    this.add = function(v) {
        if (val < 100) val += v;
    };

    
this.sub = function(v) { if (val > 0) val -= v; }; this.getVal = function() { return val; }; };

View

View作為視圖層,主要負責數據的展示。

myapp.View = function() {

    /* 視圖元素 */
    var $num = $(‘#num‘),
        $incBtn = $(‘#increase‘),
        $decBtn = $(‘#decrease‘);

    /* 渲染數據 
*/ this.render = function(model) { $num.text(model.getVal() + ‘rmb‘); }; };

現在通過Model&View完成了數據從模型層到視圖層的邏輯。但對於一個應用程序,這遠遠是不夠的,我們還需要響應用戶的操作、同步更新View和Model。於是,在MVC中引入了控制器controller,讓它來定義用戶界面對用戶輸入的響應方式,它連接模型和視圖,用於控制應用程序的流程,處理用戶的行為和數據上的改變。

MVC

那時計算機世界天地混沌,渾然一體,然後出現了一個創世者,將現實世界抽象出模型形成model,將人機交互從應用邏輯中分離形成view,然後就有了空氣、水、雞啊、蛋什麽的。
——《前端MVC變形記》

上個世紀70年代,美國施樂帕克研究中心,就是那個發明圖形用戶界面(GUI)的公司,開發了Smalltalk編程語言,並開始用它編寫圖形界面的應用程序。

到了Smalltalk-80這個版本的時候,一位叫Trygve Reenskaug的工程師為Smalltalk設計了MVC(Model-View-Controller)這種架構模式,極大地降低了GUI應用程序的管理難度,而後被大量用於構建桌面和服務器端應用程序。

技術分享

如圖,實線代表方法調用,虛線代表事件通知。

MVC允許在不改變視圖的情況下改變視圖對用戶輸入的響應方式,用戶對View的操作交給了Controller處理,在Controller中響應View的事件調用Model的接口對數據進行操作,一旦Model發生變化便通知相關視圖進行更新。

Model

Model層用來存儲業務的數據,一旦數據發生變化,模型將通知有關的視圖。

myapp.Model = function() {
    var val = 0;

    this.add = function(v) {
        if (val < 100) val += v;
    };

    this.sub = function(v) {
        if (val > 0) val -= v;
    };

    this.getVal = function() {
        return val;
    };

    /* 觀察者模式 *var self = this, 
        views = [];

    this.register = function(view) {
        views.push(view);
    };

    this.notify = function() {
        for(var i = 0; i < views.length; i++) {
            views[i].render(self);
        }
    };
};

Model和View之間使用了觀察者模式,View事先在此Model上註冊,進而觀察Model,以便更新在Model上發生改變的數據。

View

view和controller之間使用了策略模式,這裏View引入了Controller的實例來實現特定的響應策略,比如這個栗子中按鈕的 click 事件:

myapp.View = function(controller) {
    var $num = $(‘#num‘),
        $incBtn = $(‘#increase‘),
        $decBtn = $(‘#decrease‘);

    this.render = function(model) {
        $num.text(model.getVal() + ‘rmb‘);
    };

    /*  綁定事件  */
    $incBtn.click(controller.increase);
    $decBtn.click(controller.decrease);
};

如果要實現不同的響應的策略只要用不同的Controller實例替換即可。

Controller

控制器是模型和視圖之間的紐帶,MVC將響應機制封裝在controller對象中,當用戶和你的應用產生交互時,控制器中的事件觸發器就開始工作了。

myapp.Controller = function() {
    var model = null,
        view = null;

    this.init = function() {
        /* 初始化Model和View */
        model = new myapp.Model();
        view = new myapp.View(this);

        /* View向Model註冊,當Model更新就會去通知View啦 */
        model.register(view);
        model.notify();
    };

    /* 讓Model更新數值並通知View更新視圖 */
    this.increase = function() {
        model.add(1);
        model.notify();
    };

    this.decrease = function() {
        model.sub(1);
        model.notify();
    };
};

這裏我們實例化View並向對應的Model實例註冊,當Model發生變化時就去通知View做更新,這裏用到了觀察者模式。

當我們執行應用的時候,使用Controller做初始化:

(function() {
    var controller = new myapp.Controller();
    controller.init();
})();

可以明顯感覺到,MVC模式的業務邏輯主要集中在Controller,而前端的View其實已經具備了獨立處理用戶事件的能力,當每個事件都流經Controller時,這層會變得十分臃腫。而且MVC中View和Controller一般是一一對應的,捆綁起來表示一個組件,視圖與控制器間的過於緊密的連接讓Controller的復用性成了問題,如果想多個View共用一個Controller該怎麽辦呢?這裏有一個解決方案:

技術分享

來把王者榮耀壓壓驚~其實我想說的是MVP模式...

MVP

MVP(Model-View-Presenter)是MVC模式的改良,由IBM的子公司Taligent提出。和MVC的相同之處在於:Controller/Presenter負責業務邏輯,Model管理數據,View負責顯示。

技術分享

雖然在MVC裏,View是可以直接訪問Model的,但MVP中的View並不能直接使用Model,而是通過為Presenter提供接口,讓Presenter去更新Model,再通過觀察者模式更新View。

與MVC相比,MVP模式通過解耦View和Model,完全分離視圖和模型使職責劃分更加清晰;由於View不依賴Model,可以將View抽離出來做成組件,它只需要提供一系列接口提供給上層操作。

Model

myapp.Model = function() {
    var val = 0;

    this.add = function(v) {
        if (val < 100) val += v;
    };

    this.sub = function(v) {
        if (val > 0) val -= v;
    };

    this.getVal = function() {
        return val;
    };
};

Model層依然是主要與業務相關的數據和對應處理數據的方法。

View

myapp.View = function() {
    var $num = $(‘#num‘),
        $incBtn = $(‘#increase‘),
        $decBtn = $(‘#decrease‘);

    this.render = function(model) {
        $num.text(model.getVal() + ‘rmb‘);
    };

    this.init = function() {
        var presenter = new myapp.Presenter(this);

        $incBtn.click(presenter.increase);
        $decBtn.click(presenter.decrease);
    };
};

MVP定義了Presenter和View之間的接口,用戶對View的操作都轉移到了Presenter。比如這裏的View暴露setter接口讓Presenter調用,待Presenter通知Model更新後,Presenter調用View提供的接口更新視圖。

Presenter

myapp.Presenter = function(view) {
    var _model = new myapp.Model();
    var _view = view;

    _view.render(_model);

    this.increase = function() {
        _model.add(1);
        _view.render(_model);
    };

    this.decrease = function() {
        _model.sub(1);
        _view.render(_model);
    };
};

Presenter作為View和Model之間的“中間人”,除了基本的業務邏輯外,還有大量代碼需要對從View到Model和從Model到View的數據進行“手動同步”,這樣Presenter顯得很重,維護起來會比較困難。而且由於沒有數據綁定,如果Presenter對視圖渲染的需求增多,它不得不過多關註特定的視圖,一旦視圖需求發生改變,Presenter也需要改動。

運行程序時,以View為入口:

(function() {
    var view = new myapp.View();
    view.init();
})();

MVVM

MVVM(Model-View-ViewModel)最早由微軟提出。ViewModel指 "Model of View"——視圖的模型。這個概念曾在一段時間內被前端圈熱炒,以至於很多初學者拿jQuery和Vue做對比...

技術分享

MVVM把View和Model的同步邏輯自動化了。以前Presenter負責的View和Model同步不再手動地進行操作,而是交給框架所提供的數據綁定功能進行負責,只需要告訴它View顯示的數據對應的是Model哪一部分即可。

這裏我們使用Vue來完成這個栗子。

Model

在MVVM中,我們可以把Model稱為數據層,因為它僅僅關註數據本身,不關心任何行為(格式化數據由View的負責),這裏可以把它理解為一個類似json的數據對象。

var data = {
    val: 0
};

View

和MVC/MVP不同的是,MVVM中的View通過使用模板語法來聲明式的將數據渲染進DOM,當ViewModel對Model進行更新的時候,會通過數據綁定更新到View。寫法如下:

<div id="myapp">
    <div>
        <span>{{ val }}rmb</span>
    </div>
    <div>
        <button v-on:click="sub(1)">-</button>
        <button v-on:click="add(1)">+</button>
    </div>
</div>

ViewModel

ViewModel大致上就是MVC的Controller和MVP的Presenter了,也是整個模式的重點,業務邏輯也主要集中在這裏,其中的一大核心就是數據綁定,後面將會講到。與MVP不同的是,沒有了View為Presente提供的接口,之前由Presenter負責的View和Model之間的數據同步交給了ViewModel中的數據綁定進行處理,當Model發生變化,ViewModel就會自動更新;ViewModel變化,Model也會更新。

new Vue({
    el: ‘#myapp‘,
    data: data,
    methods: {
        add(v) {
            if(this.val < 100) {
                this.val += v;
            }
        },
        sub(v) {
            if(this.val > 0) {
                this.val -= v;
            }
        }
    }
});

整體來看,比MVC/MVP精簡了很多,不僅僅簡化了業務與界面的依賴,還解決了數據頻繁更新(以前用jQuery操作DOM很繁瑣)的問題。因為在MVVM中,View不知道Model的存在,ViewModel和Model也察覺不到View,這種低耦合模式可以使開發過程更加容易,提高應用的可重用性。

數據綁定

雙向數據綁定,可以簡單而不恰當地理解為一個模版引擎,但是會根據數據變更實時渲染。——《界面之下:還原真實的MV*模式》

技術分享

在Vue中,使用了雙向綁定技術(Two-Way-Data-Binding),就是View的變化能實時讓Model發生變化,而Model的變化也能實時更新到View。

“據說這玩意兒可以申請專利呢”

技術分享

不同的MVVM框架中,實現雙向數據綁定的技術有所不同。目前一些主流的前端框架實現數據綁定的方式大致有以下幾種:

  • 數據劫持 (Vue)
  • 發布-訂閱模式 (Knockout、Backbone)
  • 臟值檢查 (Angular)

我們這裏主要講講Vue。

Vue采用數據劫持&發布-訂閱模式的方式,通過ES5提供的 Object.defineProperty() 方法來劫持(監控)各屬性的 gettersetter ,並在數據(對象)發生變動時通知訂閱者,觸發相應的監聽回調。並且,由於是在不同的數據上觸發同步,可以精確的將變更發送給綁定的視圖,而不是對所有的數據都執行一次檢測。要實現Vue中的雙向數據綁定,大致可以劃分三個模塊:Observer、Compile、Watcher,如圖:

技術分享

  • Observer 數據監聽器
    負責對數據對象的所有屬性進行監聽(數據劫持),監聽到數據發生變化後通知訂閱者。

  • Compiler 指令解析器
    掃描模板,並對指令進行解析,然後綁定指定事件。

  • Watcher 訂閱者
    關聯Observer和Compile,能夠訂閱並收到屬性變動的通知,執行指令綁定的相應操作,更新視圖。Update()是它自身的一個方法,用於執行Compile中綁定的回調,更新視圖。

數據劫持

一般對數據的劫持都是通過Object.defineProperty方法進行的,Vue中對應的函數為 defineReactive ,其普通對象的劫持的精簡版代碼如下:

var foo = {
  name: ‘vue‘,
  version: ‘2.0‘
}

function observe(data) {
    if (!data || typeof data !== ‘object‘) {
        return
    }
    // 使用遞歸劫持對象屬性
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    })
}

function defineReactive(obj, key, value) {
     // 監聽子屬性 比如這裏data對象裏的 ‘name‘ 或者 ‘version‘
     observe(value)

    Object.defineProperty(obj, key, {
        get: function reactiveGetter() {
            return value
        },
        set: function reactiveSetter(newVal) {
            if (value === newVal) {
                return
            } else {
                value = newVal
                console.log(`監聽成功:${value} --> ${newVal}`)
            }
        }
    })
}

observe(foo)
foo.name = ‘angular‘ // “監聽成功:vue --> angular”

上面完成了對數據對象的監聽,接下來還需要在監聽到變化後去通知訂閱者,這需要實現一個消息訂閱器 Dep ,Watcher通過 Dep 添加訂閱者,當數據改變便觸發 Dep.notify() ,Watcher調用自己的 update() 方法完成視圖更新。

寫著寫著發現離主題越來越遠了。。。數據劫持就先講這麽多吧~對於想深入vue.js的同學可以參考勾三股四的Vue.js 源碼學習筆記

總結

MV*的目的是把應用程序的數據、業務邏輯和界面這三塊解耦,分離關註點,不僅利於團隊協作和測試,更有利於甩鍋維護和管理。業務邏輯不再關心底層數據的讀寫,而這些數據又以對象的形式呈現給業務邏輯層。從 MVC --> MVP --> MVVM,就像一個打怪升級的過程,它們都是在MVC的基礎上隨著時代和應用環境的發展衍變而來的。

在我們糾結於使用什麽架構模式或框架的時候,不如先了解它們。靜下來思考業務場景和開發需求,不同需求下會有最適合的解決方案。我們使用這個框架就代表認同它的思想,相信它能夠提升開發效率解決當前的問題,而不僅僅是因為大家都在學。

有人對新技術樂此不疲,有人對新技術不屑一顧。正如狄更斯在《雙城記》中寫的:

這是最好的時代,這是最壞的時代,這是智慧的時代,這是愚蠢的時代;這是信仰的時期,這是懷疑的時期;這是光明的季節,這是黑暗的季節;這是希望之春,這是失望之冬;人們面前應有盡有,人們面前一無所有;人們正在直登天堂;人們正在直下地獄。

請保持一顆擁抱變化的心,在新技術面前不盲目,不守舊。

一些參考資源:
GUI Architectures
界面之下:還原真實的MV*模式
前端MVC變形記
深入理解JavaScript系列
250行實現一個簡單的MVVM

淺析前端開發中的 MVC/MVP/MVVM 模式