design patterns - 從頭講解MVC模式

和通常文章不一樣,本文不依賴於任何現有的框架,也不試圖陷入冗長的發展歷史,而是徹底從頭開始,以一個儘量小可是能夠說明問題的案例,以此講清楚MVC這個歷史悠久、變型極多的技術理念。MVC是一種很是普及的,基礎的設計套路,在不一樣的語言社區內都有着大量的應用。理解了MVC,學習接下來的MVVM、MVP等才能成爲可能。html

MVC把一個系統的類分爲三種:Model,View和Controller。它們也遵循着職責分離原則:app

  1. 由Controller來處理消息
  2. 由Model掌管數據源
  3. 由View負責數據顯示

儘管MVC看起來複雜,其實用代碼表達最簡單的MVC是可能的:框架

/** 模擬 Model, View, Controller */
var M = {}, V = {}, C = {};
/** Model 負責數據 */
M.data = "hello world";
/** View 負責輸出 */
V.render = (M) => { alert(M.data); }
/** Controller 搭橋*/
C.handleOnload = () => { V.render(M); }
window.onload = C.handleOnload;
複製代碼

只要是和用戶交互的都是View,因此使用alert,或者直接輸出到console,都是一種View。函數

接下來,我但願用一個完整可是極簡的程序,來驗證MVC如何從一個平常的程序進化而來的。這個一個小型程序只不過是如此:學習

1 this

點擊按鈕會讓span加1或者減1。它簡單到你不須要分心關注,可是由足夠說明典型的html場景——就是既有數據呈現也有按鈕操做。
<div id="app">
<p><span id="count">0</span>
    <button id="inc">+</button>
    <button id="dec">-</button>
  </p>
</div>
<script>
    var counter = document.getElementById('count');
    var btn1 = document.getElementById('inc');
    var btn2 = document.getElementById('dec');
    var count = 0;
    btn1.addEventListener('click',function (){
                counter.innerHTML = ++count;
            }
    )
    btn2.addEventListener('click',function (){
                counter.innerHTML = --count;
            }
    )
</script>
複製代碼

當前的小型程序,全部的代碼,不管數據仍是邏輯仍是UI代碼,都是混合在一塊兒的,並無所謂的任何的職責分離。由於還小,問題不大。可是產品代碼都是從這一的基礎上逐步長大的。好比說數據就不太可能只有一個count,代碼逐步增大,一個對象的數據屬性會愈來愈多,隨着來的是操做數據的函數也會愈來愈多。同理包括用戶界面和業務邏輯。spa

若是使用MVC的眼光來看,在此微觀模式下,其實可使用MVC的模式作代碼的職責分離。其中全部的UI元素對象,都應該放置到View類型內,其中的事件處理都是應該放到Controller內,而數據,也就是這裏的count變量和對它的操做(減一加一),應該放置到Model類內,組裝Model和View則是Controller的職責。這樣的思路下,代碼能夠改爲:prototype

<div id="app">
<p><span id="count">1</span>
    <button id="inc">+</button>
    <button id="dec">-</button>
  </p>
</div>
<script>
    class Model{
      constructor(){
        this.count = 1
      }
      inc(){
        return this.count++
      }
      dec(){
        return this.count--
      }
    }
    class View{
      constructor(){
        this.counter = document.getElementById('count')
        this.btn1 = document.getElementById('inc')
        this.btn2 = document.getElementById('dec')
      }
      setCount(c){
       this.counter.innerHTML = c 
      }
      attachInc(cb){
        this.btn1.addEventListener('click',cb)
      }
      attachDec(cb){
        this.btn2.addEventListener('click',cb)
      }
    }
    class Controller {
      constructor(){
        this.m = new Model()
        this.v = new View()
        this.v.attachInc(this.onInc.bind(this))
        this.v.attachDec(this.onDec.bind(this))
      }
      onInc(){
        this.v.setCount(this.m.inc())
      }
      onDec(){
        this.v.setCount(this.m.dec())
      }
    }
    var c = new Controller()
</script>
複製代碼

將應用程序劃分爲三種組件,模型 - 視圖 - 控制器(MVC)設計定義它們之間的相互做用。設計

Model用於封裝數據以及對數據的處理方法。Model不依賴「View」和「Controller」,也就是說, Model 不關心它會被如何顯示或是如何被操做。Model 中數據的變化通常會經過一種刷新機制被公佈。爲此,Model須要提供被監聽的機制。code

View可以實現顯示。在 View 中通常沒有程序上的邏輯。爲了實現Model變化後的響應View 上的刷新功能,View須要監聽Model的變化。

控制器(Controller)起到不一樣層面間的組織做用,用於控制應用程序的流程。它處理事件並做出響應。「事件」包括用戶的行爲和數據 Model 上的改變。

實際上,經過觀察者模式,能夠把Model的修改刷新到多個視圖中,只要視圖作一個Model變化的訂閱便可。能夠先作一個簡單的觀察者代碼:

var Event = function (sender) {
    this._sender = sender;
    this._listeners = [];
}
Event.prototype = {
    attach: function (listener) {
        this._listeners.push(listener);
    },
    notify: function (args) {
        for (var i = 0; i < this._listeners.length; i += 1) {
            this._listeners[i](this._sender, args);
        }
    }
};
複製代碼

而後把應用稍做修改,在加上一個span,其中爲第一個span的值乘以2。界面看起來是這樣:

1|2

這就意味着,一個count的模型值,先作有兩個span須要消費它,所以不管何種緣由致使count的修改,兩個span都應該同步的被修改。此時咱們能夠利用Event對象,讓View訂閱count修改的時間,當count修改時,就會通知視圖,作相應的修改。完整的代碼以下:
<div id="app">
<p><span id="count">1</span>|<span id="count2">2</span>
    <button id="inc">+</button>
    <button id="dec">-</button>
  </p>
</div>
<script>
    class Model{
      constructor(e){
        this.e = e
        this.count = 1
      }
      inc(){
        this.count++
        this.e.notify(this.count)
        return this.count
      }
      dec(){
        this.count--
        this.e.notify(this.count)
        return this.count
      }
    }
    class View{
      constructor(e){
        this.e = e
        this.e.attach(this.f.bind(this))
        this.counter = document.getElementById('count')
        this.counter2 = document.getElementById('count2')
        this.btn1 = document.getElementById('inc')
        this.btn2 = document.getElementById('dec')
      }
      f(sender,c){
        this.counter2.innerHTML = c * 2
      }
      setCount(c){
       this.counter.innerHTML = c 
      }
      attachInc(cb){
        this.btn1.addEventListener('click',cb)
      }
      attachDec(cb){
        this.btn2.addEventListener('click',cb)
      }
    }
    var Event = function (sender) {
        this._sender = sender;
        this._listeners = [];
    }
    Event.prototype = {
        attach: function (listener) {
            this._listeners.push(listener);
        },
        notify: function (args) {
            for (var i = 0; i < this._listeners.length; i += 1) {
                this._listeners[i](this._sender, args);
            }
        }
    };
    class Controller {
      constructor(){
        this.e = new Event()
        this.m = new Model(this.e)
        this.v = new View(this.e)
        this.v.attachInc(this.onInc.bind(this))
        this.v.attachDec(this.onDec.bind(this))
      }
      onInc(){
        this.v.setCount(this.m.inc())
      }
      onDec(){
        this.v.setCount(this.m.dec())
      }
    }
    var c = new Controller()
</script>
複製代碼

此應用的最後一個實現,看起來更加具有了一個MVC的多個方面:

  1. 類職責分類爲模型視圖控制器
  2. 有了事件的發佈和訂閱的機制,能夠更好的發佈一個模型的變化到多個視圖去
  3. Model並不依賴於View,而是經過事件發佈訂閱的方式通知視圖變化
相關文章
相關標籤/搜索