首页 电脑 电脑学堂 查看内容

30行代码实现Javascript中的MVC

2016-2-23 12:13 1584 0

摘要: 从09年左右开始,MVC逐渐在前端领域大放异彩,并终于在刚刚过去的2015年随着React Native的推出而迎来大爆发:AngularJS、EmberJS、Backbone、ReactJS、RiotJS、VueJS…… 一连串的名字走马观花式的出现和更迭,它们 ...

从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之间的绑定已经初见雏形,即使需要在一个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 声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系 [邮箱地址] 删除

路过

雷人

握手
1

鲜花

鸡蛋

刚表态过的朋友 (1 人)

  • 鲜花

    匿名

最新评论

返回顶部