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

本文首發於掘金專欄,發佈於廖柯宇的獨立博客,轉載請保留原文連接。javascript

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

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

Model&View

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

咱們將依照這個「栗子」,嘗試用JavaScript實現簡單的具備MVC/MVP/MVVM模式的Web應用。java

Model

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

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做爲視圖層,主要負責數據的展現。git

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,讓它來定義用戶界面對用戶輸入的響應方式,它鏈接模型和視圖,用於控制應用程序的流程,處理用戶的行爲和數據上的改變。github

MVC

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

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

到了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

相關文章
相關標籤/搜索