MVC模式是软件工程中一种软件架构模式,一般把软件模式分为三部分,模型(Model)+视图(View)+控制器(Controller);
很多讲解MVC的例子都从一个具体的框架的某个概念入手,比如Backbone的collection或AngularJS中model,这当然不失为一个好办法。但框架之所以是框架,而不是类库(jQuery)或者工具集(Underscore),就是因为它们的背后有着众多优秀的设计理念和最佳实践,这些设计精髓相辅相成,环环相扣,缺一不可,要想在短时间内透过复杂的框架而看到某一种设计模式的本质并非是一件容易的事。
这便是这篇随笔的由来——为了帮助大家理解概念而生的原型代码,应该越简单越好,简单到刚刚足以大家理解这个概念就够了。
1.MVC的基础是观察者模式,这是实现model和view同步的关键
为了简单起见,每个model实例中只包含一个primitive value值。
1 function Model(value) { 2 this._value = typeof value === 'undefined' ? '' : value; 3 this._listeners = []; 4 } 5 Model.prototype.set = function (value) { 6 var self = this; 7 self._value = value; 8 // model中的值改变时,应通知注册过的回调函数 9 // 按照Javascript事件处理的一般机制,我们异步地调用回调函数10 // 如果觉得setTimeout影响性能,也可以采用requestAnimationFrame11 setTimeout(function () {12 self._listeners.forEach(function (listener) {13 listener.call(self, value);14 });15 });16 };17 Model.prototype.watch = function (listener) {18 // 注册监听的回调函数19 this._listeners.push(listener);20 };21 22 // html代码:23 24 // 逻辑代码:25 (function () {26 var model = new Model();27 var div1 = document.getElementById('div1');28 model.watch(function (value) {29 div1.innerHTML = value;30 });31 model.set('hello, this is a div');32 })();
借助观察者模式,我们已经实现了在调用model的set方法改变其值的时候,模板也同步更新,但这样的实现却很别扭,因为我们需要手动监听model值的改变(通过watch方法)并传入一个回调函数,有没有办法让view(一个或多个dom node)和model更简单的绑定呢?
2. 实现bind方法,绑定model和view
1 Model.prototype.bind = function (node) { 2 // 将watch的逻辑和通用的回调函数放到这里 3 this.watch(function (value) { 4 node.innerHTML = value; 5 }); 6 }; 7 8 // html代码: 9 10 11 // 逻辑代码:12 (function () {13 var model = new Model();14 model.bind(document.getElementById('div1'));15 model.bind(document.getElementById('div2'));16 model.set('this is a div');17 })();
通过一个简单的封装,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的标签属性就是一个简单有效的办法。
1 function Controller(callback) { 2 var models = {}; 3 // 找到所有有bind属性的元素 4 var views = document.querySelectorAll('[bind]'); 5 // 将views处理为普通数组 6 views = Array.prototype.slice.call(views, 0); 7 views.forEach(function (view) { 8 var modelName = view.getAttribute('bind'); 9 // 取出或新建该元素所绑定的model10 models[modelName] = models[modelName] || new Model();11 // 完成该元素和指定model的绑定12 models[modelName].bind(view);13 });14 // 调用controller的具体逻辑,将models传入,方便业务处理15 callback.call(this, models);16 }17 18 // html:19 20 21 // 逻辑代码:22 new Controller(function (models) {23 var model1 = models.model1;24 model1.set('this is a div');25 });
就这么简单吗?就这么简单:在Controller中完成业务逻辑并对Model进行修改,Model的变化触发View的自动更新,怎么样,算得上一个有模有样的MVC吧?当然,这样的”框架”还不足以用于生产环境,不过如果它能或多或少地帮助到大家对于MVC的理解的话,博主就非常满足了。
整理后去掉注释的”框架”代码:
1 function Model(value) { 2 this._value = typeof value === 'undefined' ? '' : value; 3 this._listeners = []; 4 } 5 Model.prototype.set = function (value) { 6 var self = this; 7 self._value = value; 8 setTimeout(function () { 9 self._listeners.forEach(function (listener) {10 listener.call(self, value);11 });12 });13 };14 Model.prototype.watch = function (listener) {15 this._listeners.push(listener);16 };17 Model.prototype.bind = function (node) {18 this.watch(function (value) {19 node.innerHTML = value;20 });21 };22 function Controller(callback) {23 var models = {};24 var views = Array.prototype.slice.call(document.querySelectorAll('[bind]'), 0);25 views.forEach(function (view) {26 var modelName = view.getAttribute('bind');27 (models[modelName] = models[modelName] || new Model()).bind(view);28 });29 callback.call(this, models);[mw_shl_code=applescript,true] : : 30 // controller:31 new Controller(function (models) {32 function setTime() {33 var date = new Date();34 models.hour.set(date.getHours());35 models.minute.set(date.getMinutes());36 models.second.set(date.getSeconds());37 }38 setTime();39 setInterval(setTime, 1000);40 });
1 function Model(value) { 2 this._value = typeof value === 'undefined' ? '' : value; 3 this._listeners = []; 4 } 5 Model.prototype.set = function (value) { 6 var self = this; 7 self._value = value; 8 setTimeout(function () { 9 self._listeners.forEach(function (listener) {10 listener.call(self, value);11 });12 });13 };14 Model.prototype.watch = function (listener) {15 this._listeners.push(listener);16 };17 Model.prototype.bind = function (node) {18 this.watch(function (value) {19 node.innerHTML = value;20 });21 };22 function Controller(callback) {23 var models = {};24 var views = Array.prototype.slice.call(document.querySelectorAll('[bind]'), 0);25 views.forEach(function (view) {26 var modelName = view.getAttribute('bind');27 (models[modelName] = models[modelName] || new Model()).bind(view);28 });29 callback.call(this, models);[mw_shl_code=applescript,true] : : 30 // controller:31 new Controller(function (models) {32 function setTime() {33 var date = new Date();34 models.hour.set(date.getHours());35 models.minute.set(date.getMinutes());36 models.second.set(date.getSeconds());37 }38 setTime();39 setInterval(setTime, 1000);40 });
4. 一个简单的例子
下面请大家看一个简单例子,如何实现电子表
// html:
可以看出,controller中只负责更新model的逻辑,和view完全解耦;而view和model的绑定是通过view中的属性和框架中controller的初始化代码完成的,也没有出现在业务逻辑中;至于view的更新,也是通过框架中的观察者模式实现的。