文档章节

30行代码实现Javascript中的MVC

远方__
 远方__
发布于 2016/12/23 12:35
字数 2121
阅读 6
收藏 0

从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');
});

就这么简单吗?就这么简单。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).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的更新,也是通过框架中的观察者模式实现的。

后记:

笔者在学习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

……

本文转载自:http://blog.csdn.net/sinat_27615265/article/details/50675700

远方__
粉丝 0
博文 82
码字总数 0
作品 0
丰台
程序员
私信 提问
运用 Ext JS 4 的 MVC 架构

Ext JS 4 简介 Ext JS 4 目前是 Sencha 的产品,4.x 的正式版本号是 4.0.7。Ext JS 4 提供商业版本,但如果您的项目是开源的,则可以免费使用 Ext JS 4。Ext JS 的论坛目前非常活跃;Ext JS ...

IBMdW
2012/10/16
871
3
分享16个javascript&jQuery的MVC教程

日期:2011/12/19 来源:GBin1.com 2011即将过去我们将在几周后迎来新的一年。 这里我们将介绍10多个js和jQuery的MVC教程希望大家喜欢! jQuery MVC 1. AJAX - Spring MVC 3和jQuery的整合 ...

gbin1
2011/12/21
5.7K
5
JavaScript MVC 开源框架

腾讯Web前端框架库 JX by Tencent AlloyTeam 简介 JX 是模块化的非侵入式Web前端框架,开发于2008年,并于2009年开源于GoogleCode - http://code.google.com/p/j-et/,于2012年切换到Github,...

wewelove
2014/06/04
345
0
C# 代码中调用 Javascript 代码段以提高应用程序的配置灵活性(使用 Javascript .NET 与 Jint)

一般来说,我们需要在开发应用软件的配置文件中,添加一些参数,用于后续用户根据实际情况,自行调整。 配置参数,可以放在配置文件中、环境变量中、或数据库表中(如果使用了数据库的话)。通...

Jacklondon Chen
2018/07/30
0
0
「译」JavaScript 的 MVC 模式

原文:Model-View-Controller (MVC) with JavaScript 作者:Alex@Net 译文:JavaScript 的 MVC 模式 译者:justjavac 本文介绍了模型-视图-控制器模式在 JavaScript 中的实现。 我喜欢 Java...

justjavac
2012/12/18
3.9K
9

没有更多内容

加载失败,请刷新页面

加载更多

centos 查看删除旧内核

1、查看系统中安装的内核 $ yum list installed | grep kernel 2、删除系统中旧内核 $ yum install yum-utils$ package-cleanup --oldkernels --count=2...

编程老陆
29分钟前
7
0
ES6

ES6:不改变原理的基础上,让API变得更简单 一、let:代替var用于声明变量 1、var的缺点: (1)声明提前 (2)没有块级作用域 2、let的优点: (1)组织了申明提前 (2)让let所在的块({}),...

wytao1995
今天
3
0
kubernetes 环境搭建 —— minikube

创建集群 minikube start 搭建好 k8s 集群后,可以查看集群的状态以及部署应用。主要用到的是 k8s 的 api,这通常需借助于 kutectl 命令行工具 基本操作 kubectl versionkubectl cluster-i...

lemos
今天
9
0
关于js混淆与反混淆还原操作

使用js的混淆加密,其目的是为了保护我们的前端代码逻辑,对应一些搞技术吃饭的公司来说,为了防止被竞争对手抓取或使用自己的代码,就会考虑如何加密,或者混淆js来达到代码保护。 1、为什么...

开源oschina
今天
11
0
用盛金公式解三次方程(ansi c版)

/* cc cubic.c -lm gcc cubic.c -lm Shengjin's Formulas Univariate cubic equation aX ^ 3 + bX ^ 2 + cX + d = 0, (a, b, c, d < R, and a!= 0). Multiple root disc......

wangxuwei
今天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部