30行代碼實現JavaScript中的MVC

從09年左右開始,MVC逐漸在前端領域大放異彩,並終於在剛剛過去的2015年隨着React Native的推出而迎來大爆發:
AngularJS、EmberJS、Backbone、ReactJS、RiotJS、VueJS…… 一連串的名字蜻蜓點水式的出現和更迭,
它們中一些已經漸漸淡出了你們的視野,一些還在迅速茁壯成長,一些則已經在特定的生態環境中獨當一面捨我其誰。
但不論如何,MVC已經並將持續深入地影響前端工程師們的思惟方式和工做方法。javascript

不少講解MVC的例子都從一個具體的框架的某個概念入手,好比Backbone的collection或AngularJS中model,這固然不失爲一個好辦法。
但框架之因此是框架,而不是類庫(jQuery)或者工具集(Underscore),就是由於它們的背後有着衆多優秀的設計理念和最佳實踐,
這些設計精髓相輔相成,環環相扣,缺一不可,要想在短期內透過複雜的框架而看到某一種設計模式的本質並不是是一件容易的事。
這即是這篇隨筆的由來——爲了幫助你們理解概念而生的原型代碼,應該越簡單越好,簡單到剛剛足以你們理解這個概念就夠了。css

1. MVC的基礎是觀察者模式,這是實現model和view同步的關鍵
爲了簡單起見,每一個model實例中只包含一個primitive value值。html

function Model(value) {
    this._value = typeof value === 'undefined' ? '' : value;
    this._listeners = [];
}
Model.prototype.set = function (value) {
    var self = this;
    self._value = value;
    // model中的值改變時,應通知註冊過的回調函數
    // 按照Javascript事件處理的通常機制,咱們異步地調用回調函數
    // 若是以爲setTimeout影響性能,也能夠採用requestAnimationFrame
    setTimeout(function () {
        self._listeners.forEach(function (listener) {
            listener.call(self, value);
        });
    });
};
Model.prototype.watch = function (listener) {
    // 註冊監聽的回調函數
    this._listeners.push(listener);
};
// html代碼:
<div id="div1"></div>
// 邏輯代碼:
(function () {
    var model = new Model();
    var div1 = document.getElementById('div1');
    model.watch(function (value) {
        div1.innerHTML = value;
    });
    model.set('hello, this is a div');
})();

藉助觀察者模式,咱們已經實現了在調用model的set方法改變其值的時候,模板也同步更新,但這樣的實現卻很彆扭,
由於咱們須要手動監聽model值的改變(經過watch方法)並傳入一個回調函數,有沒有辦法讓view(一個或多個dom node)和model更簡單的綁定呢?前端

2. 實現bind方法,綁定model和viewjava

Model.prototype.bind = function (node) {
    // 將watch的邏輯和通用的回調函數放到這裏
    this.watch(function (value) {
        node.innerHTML = value;
    });
};
// html代碼:
<div id="div1"></div>
<div id="div2"></div>
// 邏輯代碼:
(function () {
    var model = new Model();
    model.bind(document.getElementById('div1'));
    model.bind(document.getElementById('div2'));
    model.set('this is a div');
})();

經過一個簡單的封裝,view和model之間的綁定已經初見雛形,即便須要綁定多個view,實現起來也很輕鬆。
注意bind是Function類prototype上的一個原生方法,不過它和MVC的關係並不緊密,筆者又實在太喜歡bind這個單詞,
一語中的,言簡意賅,因此索性在這裏把原生方法覆蓋了,你們能夠忽略。言歸正傳,雖然綁定的複雜度下降了,
這一步依然要依賴咱們手動完成,有沒有可能把綁定的邏輯從業務代碼中完全解耦呢?node

3. 實現controller,將綁定從邏輯代碼中解耦
細心的朋友可能已經注意到,雖然講的是MVC,可是上文中卻只出現了Model類,View類不出現能夠理解,
畢竟HTML就是現成的View(事實上本文中從始至終也只是利用HTML做爲View,javascript代碼中並無出現過View類),
那Controller類爲什麼也隱身了呢?別急,其實所謂的」邏輯代碼」就是一個框架邏輯(姑且將本文的原型玩具稱之爲框架)和業務邏輯耦合度很高的代碼段,
如今咱們就來將它分解一下。redux

若是要將綁定的邏輯交給框架完成,那麼就須要告訴框架如何來完成綁定。因爲JS中較難完成annotation(註解),
咱們能夠在view中作這層標記——使用html的標籤屬性就是一個簡單有效的辦法。設計模式

function Controller(callback) {
    var models = {};
    // 找到全部有bind屬性的元素
    var views = document.querySelectorAll('[bind]');
    // 將views處理爲普通數組
    views = Array.prototype.slice.call(views, 0);
    views.forEach(function (view) {
        var modelName = view.getAttribute('bind');
        // 取出或新建該元素所綁定的model
        models[modelName] = models[modelName] || new Model();
        // 完成該元素和指定model的綁定
        models[modelName].bind(view);
    });
    // 調用controller的具體邏輯,將models傳入,方便業務處理
    callback.call(this, models);
}
// html:
<div id="div1" bind="model1"></div>
<div id="div2" bind="model1"></div>
// 邏輯代碼:
new Controller(function (models) {
    var model1 = models.model1;
    model1.set('this is a div');
});

就這麼簡單嗎?就這麼簡單。MVC的本質就是在controller中完成業務邏輯,並對model進行修改,同時model的改變引發view的自動更新,
這些邏輯在上面的代碼中都有所體現,而且支持多個view、多個model。雖然不足以用於生產項目,可是但願對你們的MVC學習多少有些幫助。數組

整理後去掉註釋的」框架」代碼:安全

function Model(value) {
    this._value = typeof value === 'undefined' ? '' : value;
    this._listeners = [];
}
Model.prototype.set = function (value) {
    var self = this;
    self._value = value;
    setTimeout(function () {
        self._listeners.forEach(function (listener) {
            listener.call(self, value);
        });
    });
};
Model.prototype.watch = function (listener) {
    this._listeners.push(listener);
};
Model.prototype.bind = function (node) {
    this.watch(function (value) {
        node.innerHTML = value;
    });
};
function Controller(callback) {
    var models = {};
    var views = Array.prototype.slice.call(document.querySelectorAll('[bind]'), 0);
    views.forEach(function (view) {
        var modelName = view.getAttribute('bind');
        models[modelName] = models[modelName] || new Model();
        models[modelName].bind(view);
    });
    callback.call(this, models);
}

後記:

筆者在學習Flux和redux的過程當中,雖然掌握了工具的使用方法,但只是知其然而不知其因此然,
對ReactJS官方文檔中一直強調的 「Flux eschews MVC in favor of a unidirectional data flow」 不甚理解,
始終以爲單向數據流和MVC並不衝突,不明白爲何在ReactJS的文檔中這兩者會被對立起來,有他無我,有我無他(eschew,避開)。
終於下定決心,回到MVC的定義上從新研究,雖然平日工做裏大大咧咧複製粘貼,可是我們偶爾也得任性一把,咬文嚼字一番,對吧?
這樣的方式也的確幫助了我對於這句話的理解,這裏能夠把本身的思考分享給你們:之因此以爲MVC和flux中的單向數據流類似,
多是由於沒有區分清楚MVC和觀察者模式的關係形成的——MVC是基於觀察者模式的,flux也是,所以這種類似性的由來是觀察者模式,
而不是MVC和flux自己。這樣的理解也在四人組的設計模式原著中獲得了印證:
」The first and perhaps best-known example of the Observer pattern appears in Smalltalk Model/View/Controller (MVC),
the user interface framework in the Smalltalk environment [KP88]. MVC’s Model class plays the role of Subject,
while View is the base class for observers. 」。

若是讀者有興趣在這樣一個原型玩具的基礎上繼續拓展,能夠參考下面的一些方向:

  1. 實現對input類標籤的雙向綁定
  2. 實現對controller所控制的scope的精準控制,這裏一個controller就控制了整個dom樹
  3. 實現view層有關dom node隱藏/顯示、建立/銷燬的邏輯
  4. 集成virtual dom,增長dom diff的功能,提升渲染效率
  5. 提供依賴注入功能,實現控制反轉
  6. 對innerHTML的賦值內容進行安全檢查,防止惡意注入
  7. 實現model collection的邏輯,這裏每一個model只有一個值
  8. 利用es5中的setter改變set方法的實現,使得對model的修改更加簡單
  9. 在view層中增長對屬性和css的控制
  10. 支持相似AngularJS中雙大括號的語法,只綁定部分html
相關文章
相關標籤/搜索