30行程式碼實現Javascript中的MVC

才智咖 人氣:1.77W
一連串的名字走馬觀花式的出現和更迭,它們中一些已經漸漸淡出了大家的視野,一些還在迅速茁壯成長,一些則已經在特定的生態環境中獨當一面捨我其誰。但不論如何,MVC已經並將持續深刻地影響前端工程師們的思維方式和工作方法。

很多講解MVC的例子都從一個具體的框架的某個概念入手,比如Backbone的collection或AngularJS中model,這當然不失為一個好辦法。但框架之所以是框架,而不是類庫(jQuery)或者工具集(Underscore),就是因為它們的背後有著眾多優秀的設計理念和最佳實踐,這些設計精髓相輔相成,環環相扣,缺一不可,要想在短時間內透過複雜的框架而看到某一種設計模式的本質並非是一件容易的事。

30行程式碼實現Javascript中的MVC

這便是這篇隨筆的由來——為了幫助大家理解概念而生的原型程式碼,應該越簡單越好,簡單到剛剛足以大家理解這個概念就夠了。

 

1.MVC的基礎是觀察者模式,這是實現model和view同步的關鍵

 

為了簡單起見,每個model例項中只包含一個primitive value值。

 

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和view

 

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之間的繫結已經初見雛形,即使需要在一個model上繫結多個view,實現起來也很輕鬆。注意bind是Function類prototype上的一個原生方法,不過它和MVC的關係並不緊密,筆者又實在太喜歡bind這個單詞,一語中的,言簡意賅,所以索性在這裡把原生方法覆蓋了,大家可以忽略。言歸正傳,雖然繫結的複雜度降低了,這一步依然要依賴我們手動完成,有沒有可能把繫結的邏輯從業務程式碼中徹底解耦呢?

 

3. 實現controller,將繫結從邏輯程式碼中解耦

 

細心的朋友可能已經注意到,雖然講的是MVC,但是上文中卻只出現了Model類,View類不出現可以理解,畢竟HTML就是現成的View(事實上本文中從始至終也只是利用HTML作為View,javascript程式碼中並沒有出現過View類),那Controller類為何也隱身了呢?別急,其實所謂的”邏輯程式碼”就是一個框架邏輯(姑且將本文的原型玩具稱之為框架)和業務邏輯耦合度很高的程式碼段,現在我們就來將它分解一下。

 

如果要將繫結的邏輯交給框架完成,那麼就需要告訴框架如何來完成繫結。由於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');

});

 

就這麼簡單嗎?就這麼簡單:在Controller中完成業務邏輯並對Model進行修改,Model的`變化觸發View的自動更新,怎麼樣,算得上一個有模有樣的MVC吧?當然,這樣的”框架”還不足以用於生產環境,不過如果它能或多或少地幫助到大家對於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()).bind(view);

    });

    callback.call(this, models);

}

 

4. 一個簡單的例子

 

下面請大家看一個簡單例子,如何實現電子

 

// html:

<span bind="hour"></span> : <span bind="minute"></span> : <span bind="second"></span>

// controller:

new Controller(function (models) {

    function setTime() {

        var date = new Date();

        models.hour.set(date.getHours());

        models.minute.set(date.getMinutes());

        models.second.set(date.getSeconds());

    }

    setTime();

    setInterval(setTime, 1000);

});

 

可以看出,controller中只負責更新model的邏輯,和view完全解耦;而view和model的繫結是通過view中的屬性和框架中controller的初始化程式碼完成的,也沒有出現在業務邏輯中;至於view的更新,也是通過框架中的觀察者模式實現的。


TAGS:MVC 程式碼