仅30行代码实现Javascript中的MVC


Posted in Javascript onFebruary 15, 2016

从09年左右开始,MVC逐渐在前端领域大放异彩,并终于在刚刚过去的2015年随着React Native的推出而迎来大爆发:AngularJS、EmberJS、Backbone、ReactJS、RiotJS、VueJS…… 一连串的名字走马观花式的出现和更迭,它们中一些已经渐渐淡出了大家的视野,一些还在迅速茁壮成长,一些则已经在特定的生态环境中独当一面舍我其谁。但不论如何,MVC已经并将持续深刻地影响前端工程师们的思维方式和工作方法。

很多讲解MVC的例子都从一个具体的框架的某个概念入手,比如Backbone的collection或AngularJS中model,这当然不失为一个好办法。但框架之所以是框架,而不是类库(jQuery)或者工具集(Underscore),就是因为它们的背后有着众多优秀的设计理念和最佳实践,这些设计精髓相辅相成,环环相扣,缺一不可,要想在短时间内透过复杂的框架而看到某一种设计模式的本质并非是一件容易的事。

这便是这篇随笔的由来——为了帮助大家理解概念而生的原型代码,应该越简单越好,简单到刚刚足以大家理解这个概念就够了。

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之间的绑定已经初见雏形,即使需要绑定多个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');
});

就这么简单吗?就这么简单。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
  • ……

一个完善的框架要经过无数的提炼和修改,这里只是最初最初的第一步,道路还很漫长,希望大家再接再厉。

Javascript 相关文章推荐
js利用div背景,做一个竖线的效果。
Nov 22 Javascript
javascript 判断数组是否已包含了某个元素的函数
May 30 Javascript
基于jquery的使ListNav兼容中文首字拼音排序的实现代码
Jul 10 Javascript
ASP.NET jQuery 实例6 (实现CheckBoxList成员全选或全取消)
Jan 13 Javascript
JavaScript中的undefined学习总结
Nov 30 Javascript
javascript获得当前的信息的一些常用命令
Feb 25 Javascript
jQuery网页版打砖块小游戏源码分享
Aug 20 Javascript
Vue方法与事件处理器详解
Dec 01 Javascript
node.js 抓取代理ip实例代码
Apr 30 Javascript
javascript Function函数理解与实战
Dec 01 Javascript
从vue源码看props的用法
Jan 09 Javascript
vue实现滑动解锁功能
Mar 03 Vue.js
理解javascript中的with关键字
Feb 15 #Javascript
使用基于Node.js的构建工具Grunt来发布ASP.NET MVC项目
Feb 15 #Javascript
JavaScript模版引擎的基本实现方法浅析
Feb 15 #Javascript
在ASP.NET MVC项目中使用RequireJS库的用法示例
Feb 15 #Javascript
一道常被人轻视的web前端常见面试题(JS)
Feb 15 #Javascript
获取阴历(农历)和当前日期的js代码
Feb 15 #Javascript
极易被忽视的javascript面试题七问七答
Feb 15 #Javascript
You might like
wiki-shan写的php在线加密的解密程序
2008/09/07 PHP
优化php效率,提高php性能的一些方法
2011/03/24 PHP
用PHP的超级变量$_GET获取HTML表单(Form) 数据
2011/05/07 PHP
php对大文件进行读取操作的实现代码
2013/01/23 PHP
获取PHP警告错误信息的解决方法
2013/06/03 PHP
PHP中把错误日志保存在系统日志中(Windows系统)
2015/06/23 PHP
详解PHP 7.4 中数组延展操作符语法知识点
2019/07/19 PHP
jquery cookie插件代码类
2009/05/26 Javascript
JQuery的html(data)方法与&amp;lt;script&amp;gt;脚本块的解决方法
2010/03/09 Javascript
一个JavaScript函数把URL参数解析成Json对象
2014/09/24 Javascript
javascript下拉框选项单击事件的例子分享
2015/03/04 Javascript
Jquery Mobile 自定义按钮图标
2015/11/18 Javascript
JavaScript 正则表达式中global模式的特性
2016/02/25 Javascript
javascript滚轮控制模拟滚动条
2016/10/19 Javascript
jQuery中 bind的用法简单介绍
2017/02/13 Javascript
ES6中字符串string常用的新增方法小结
2017/11/07 Javascript
搭建element-ui的Vue前端工程操作实例
2018/02/23 Javascript
基于VuePress 轻量级静态网站生成器的实现方法
2018/04/17 Javascript
nodejs初始化init的示例代码
2018/10/10 NodeJs
vue视频播放暂停代码
2019/11/08 Javascript
用Python和MD5实现网站挂马检测程序
2014/03/13 Python
Python实现变量数值交换及判断数组是否含有某个元素的方法
2017/09/18 Python
python分布式环境下的限流器的示例
2017/10/26 Python
Python中is和==的区别详解
2018/11/15 Python
python 有效的括号的实现代码示例
2019/11/11 Python
wxPython实现分隔窗口
2019/11/19 Python
Windows下pycharm安装第三方库失败(通用解决方案)
2020/09/17 Python
Hammitt官网:设计师手袋
2020/05/23 全球购物
学生会竞选自荐信
2013/10/12 职场文书
经济与贸易专业应届生求职信
2013/11/19 职场文书
初三学习决心书
2014/03/11 职场文书
党员服务承诺书
2014/05/28 职场文书
政风行风评议心得体会
2014/10/21 职场文书
Java数据开发辅助工具Docker与普通程序使用方法
2021/09/15 Java/Android
Java中Quartz高可用定时任务快速入门
2022/04/03 Java/Android
python DataFrame中stack()方法、unstack()方法和pivot()方法浅析
2022/04/06 Python