僅30行代碼實現(xiàn)Javascript中的MVC
從09年左右開始,MVC逐漸在前端領域大放異彩,并終于在剛剛過去的2015年隨著React Native的推出而迎來大爆發(fā):AngularJS、EmberJS、Backbone、ReactJS、RiotJS、VueJS…… 一連串的名字走馬觀花式的出現(xiàn)和更迭,它們中一些已經(jīng)漸漸淡出了大家的視野,一些還在迅速茁壯成長,一些則已經(jīng)在特定的生態(tài)環(huán)境中獨當一面舍我其誰。但不論如何,MVC已經(jīng)并將持續(xù)深刻地影響前端工程師們的思維方式和工作方法。
很多講解MVC的例子都從一個具體的框架的某個概念入手,比如Backbone的collection或AngularJS中model,這當然不失為一個好辦法。但框架之所以是框架,而不是類庫(jQuery)或者工具集(Underscore),就是因為它們的背后有著眾多優(yōu)秀的設計理念和最佳實踐,這些設計精髓相輔相成,環(huán)環(huán)相扣,缺一不可,要想在短時間內(nèi)透過復雜的框架而看到某一種設計模式的本質(zhì)并非是一件容易的事。
這便是這篇隨筆的由來——為了幫助大家理解概念而生的原型代碼,應該越簡單越好,簡單到剛剛足以大家理解這個概念就夠了。
1. MVC的基礎是觀察者模式,這是實現(xiàn)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中的值改變時,應通知注冊過的回調(diào)函數(shù)
// 按照Javascript事件處理的一般機制,我們異步地調(diào)用回調(diào)函數(shù)
// 如果覺得setTimeout影響性能,也可以采用requestAnimationFrame
setTimeout(function () {
self._listeners.forEach(function (listener) {
listener.call(self, value);
});
});
};
Model.prototype.watch = function (listener) {
// 注冊監(jiān)聽的回調(diào)函數(shù)
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');
})();
借助觀察者模式,我們已經(jīng)實現(xiàn)了在調(diào)用model的set方法改變其值的時候,模板也同步更新,但這樣的實現(xiàn)卻很別扭,因為我們需要手動監(jiān)聽model值的改變(通過watch方法)并傳入一個回調(diào)函數(shù),有沒有辦法讓view(一個或多個dom node)和model更簡單的綁定呢?
2. 實現(xiàn)bind方法,綁定model和view
Model.prototype.bind = function (node) {
// 將watch的邏輯和通用的回調(diào)函數(shù)放到這里
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之間的綁定已經(jīng)初見雛形,即使需要綁定多個view,實現(xiàn)起來也很輕松。注意bind是Function類prototype上的一個原生方法,不過它和MVC的關系并不緊密,筆者又實在太喜歡bind這個單詞,一語中的,言簡意賅,所以索性在這里把原生方法覆蓋了,大家可以忽略。言歸正傳,雖然綁定的復雜度降低了,這一步依然要依賴我們手動完成,有沒有可能把綁定的邏輯從業(yè)務代碼中徹底解耦呢?
3. 實現(xiàn)controller,將綁定從邏輯代碼中解耦
細心的朋友可能已經(jīng)注意到,雖然講的是MVC,但是上文中卻只出現(xiàn)了Model類,View類不出現(xiàn)可以理解,畢竟HTML就是現(xiàn)成的View(事實上本文中從始至終也只是利用HTML作為View,javascript代碼中并沒有出現(xiàn)過View類),那Controller類為何也隱身了呢?別急,其實所謂的"邏輯代碼"就是一個框架邏輯(姑且將本文的原型玩具稱之為框架)和業(yè)務邏輯耦合度很高的代碼段,現(xiàn)在我們就來將它分解一下。
如果要將綁定的邏輯交給框架完成,那么就需要告訴框架如何來完成綁定。由于JS中較難完成annotation(注解),我們可以在view中做這層標記——使用html的標簽屬性就是一個簡單有效的辦法。
function Controller(callback) {
var models = {};
// 找到所有有bind屬性的元素
var views = document.querySelectorAll('[bind]');
// 將views處理為普通數(shù)組
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);
});
// 調(diào)用controller的具體邏輯,將models傳入,方便業(yè)務處理
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的本質(zhì)就是在controller中完成業(yè)務邏輯,并對model進行修改,同時model的改變引起view的自動更新,這些邏輯在上面的代碼中都有所體現(xiàn),并且支持多個view、多個model。雖然不足以用于生產(chǎn)項目,但是希望對大家的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官方文檔中一直強調(diào)的 "Flux eschews MVC in favor of a unidirectional data flow" 不甚理解,始終覺得單向數(shù)據(jù)流和MVC并不沖突,不明白為什么在ReactJS的文檔中這二者會被對立起來,有他無我,有我無他(eschew,避開)。終于下定決心,回到MVC的定義上重新研究,雖然平日工作里大大咧咧復制粘貼,但是咱們偶爾也得任性一把,咬文嚼字一番,對吧?這樣的方式也的確幫助了我對于這句話的理解,這里可以把自己的思考分享給大家:之所以覺得MVC和flux中的單向數(shù)據(jù)流相似,可能是因為沒有區(qū)分清楚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. "。
如果讀者有興趣在這樣一個原型玩具的基礎上繼續(xù)拓展,可以參考下面的一些方向:
- 1. 實現(xiàn)對input類標簽的雙向綁定
- 2. 實現(xiàn)對controller所控制的scope的精準控制,這里一個controller就控制了整個dom樹
- 3. 實現(xiàn)view層有關dom node隱藏/顯示、創(chuàng)建/銷毀的邏輯
- 4. 集成virtual dom,增加dom diff的功能,提高渲染效率
- 5. 提供依賴注入功能,實現(xiàn)控制反轉(zhuǎn)
- 6. 對innerHTML的賦值內(nèi)容進行安全檢查,防止惡意注入
- 7. 實現(xiàn)model collection的邏輯,這里每個model只有一個值
- 8. 利用es5中的setter改變set方法的實現(xiàn),使得對model的修改更加簡單
- 9. 在view層中增加對屬性和css的控制
- 10.支持類似AngularJS中雙大括號的語法,只綁定部分html
- ……
一個完善的框架要經(jīng)過無數(shù)的提煉和修改,這里只是最初最初的第一步,道路還很漫長,希望大家再接再厲。
- 使用jQuery向asp.net Mvc傳遞復雜json數(shù)據(jù)-ModelBinder篇
- Extjs4.1.x 框架搭建 采用Application動態(tài)按需加載MVC各模塊完美實現(xiàn)
- 談談關于JavaScript 中的 MVC 模式
- MVC后臺創(chuàng)建Json(List)前臺接受并循環(huán)讀取實例
- 教你如何在 Javascript 文件里使用 .Net MVC Razor 語法
- Javascript MVC框架Backbone.js詳解
- .Net基于MVC4 Web Api輸出Json格式實例
- ASP.NET中MVC使用AJAX調(diào)用JsonResult方法并返回自定義錯誤信息
相關文章
JS實現(xiàn)無縫循環(huán)marquee滾動效果
這篇文章主要為大家詳細介紹了JS實現(xiàn)無縫循環(huán)marquee滾動效果,兼容IE, FireFox, Chrome,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
詳解Webpack loader 之 file-loader
這篇文章主要介紹了詳解Webpack loader 之 file-loader,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11
layui.use模塊外部使用其內(nèi)部定義的js封裝函數(shù)方法
今天小編就為大家分享一篇layui.use模塊外部使用其內(nèi)部定義的js封裝函數(shù)方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09
拖動table標題實現(xiàn)改變td的大小(css+js代碼)
拖動列寬的表格table標題同時改變td的大小,本文將以實例演示為大家呈現(xiàn),感興趣的朋友可以參考下哈,希望對你學習js或者css有所幫助2013-04-04
本地圖片預覽(支持IE6/IE7/IE8/Firefox3)經(jīng)驗總結(jié)
遇到的本地圖片預覽的需求,IE6下可以直接從file的value獲取圖片路徑來顯示預覽,IE7和IE8下通過select獲取file的圖片路徑,再用濾鏡來顯示預覽,至于FireFox祥看本文吧,希望可以幫助到你2013-03-03
使用documentElement正確取得當前可見區(qū)域的大小
如何取得當前瀏覽器里面可見區(qū)域的大?。科渌椒ǘ疾贿m用,只有documentElement才可以,需要的朋友可以參考下2014-07-07

