30 行代码实现 JS 中的 MVC

一连串的名字走马观花式的出现和更迭,它们中一些已经渐渐淡出了大家的视野,一些还在迅速茁壮成长,一些则已经在特定的生态环境中独当一面舍我其谁。但不论如何,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 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

……

一个完善的框架要经过无数的提炼和修改,这里只是最初最初的第一步,不过别忘了,我们的征程是星辰大海,哈哈

时间: 2024-08-02 01:55:47

30 行代码实现 JS 中的 MVC的相关文章

30行代码实现JavaScript中的MVC

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

简单谈谈js中的MVC

MVC是什么? MVC是一种架构模式,它将应用抽象为3个部分:模型(数据).视图.控制器(分发器). 本文将用一个经典的例子todoList来展开(代码在最后). 一个事件发生的过程(通信单向流动): 1.用户在视图 V 上与应用程序交互 2.控制器 C 触发相应的事件,要求模型 M 改变状态(读写数据) 3.模型 M 将数据发送到视图 V ,更新数据,展现给用户 在js的传统开发模式中,大多基于事件驱动的: 1.hash驱动 2.DOM事件,用来驱动视图 3.模型事件(业务模型事件和数据模型事

一个只有99行代码的JS流程框架(二)

张镇圳,腾讯Web前端高级工程师,对内部系统前端建设有多年经验,喜欢钻研捣鼓各种前端组件和框架. 导语 前面写了一篇文章,叫<一个只有99行代码的JS流程框架>,虽然该框架基本已经能实现一个流程正常的逻辑流转,但是在分模块应用下还是缺少一定的能力,无法将一个页面中的不同模块很好的连接在一起,于是对之前的框架进行了升级,新增了子流程的概念. 子流程 什么是子流程?在这个升级后的框架里(当然代码已经不止99行了,不要在乎标题),每个步骤不但可以是一个function,还可以引用另一个流程,这个被引

浅谈js中的MVC

MVC是什么? MVC是一种架构模式,它将应用抽象为3个部分:模型(数据).视图.控制器(分发器) 本文将用一个经典的例子todoList来展开 一个事件发生的过程(通信单向流动): 1.用户在视图V上与应用程序交互 2.控制器C触发相应的事件,要求模型M改变状态(读写数据) 3.模型M将数据发送到视图V,更新数据,展现给用户 在js的传统开发模式中,大多基于事件驱动的: 1.hash驱动 2.DOM事件,用来驱动视图 3.模型事件(业务模型事件和数据模型事件),用来驱动模型和模型结合 所以js

30 行代码绘出你的微信朋友统计图

前言 大家好,这里是「brucepk」爬虫 系列教程.此文首发于「brucepk」公众号,欢迎大家关注.此系列教程以实例项目为材料进行分析,从项目中学习 python 爬虫,跟着我一起学习,每天进步一点点. 学编程是一件枯燥的事情,比较好的方法是在实际项目中学习成长.今天带来的是 30 行代码画出你的微信朋友的性别统计图. 最近发现一个有意思的库:itchat,itchat 是一个开源的微信个人号接口.今天就用 itchat 来统计自己微信好友性别的比例并用柱形图展示出来. 项目环境:pytho

30行代码构建javascript 的MVC模式

MVC模式是软件工程中一种软件架构模式,一般把软件模式分为三部分,模型(Model)+视图(View)+控制器(Controller); 很多讲解MVC的例子都从一个具体的框架的某个概念入手,比如Backbone的collection或AngularJS中model,这当然不失为一个好办法.但框架之所以是框架,而不是类库(jQuery)或者工具集(Underscore),就是因为它们的背后有着众多优秀的设计理念和最佳实践,这些设计精髓相辅相成,环环相扣,缺一不可,要想在短时间内透过复杂的框架而看

一个只有99行代码的JS流程框架

最近一直在想一个问题,如何能让js代码写起来更语义化和更具有可读性. 上周末的时候突发奇想,当代码在运行的时候,其实跟我们做事情是类似的,都是做完一步接着下一步,并且这些事情有些是可规划的,有些是需要做完该步才知道下一步该做什么.想到这里一个js框架雏形在我大脑中慢慢形成,暂且命名为flowJS. 接着说说这个框架应该有哪些API? 1.可以预先规划好流程的每一步,如this.setNext('步骤A').setNext('步骤B')-- 2.可以在任何一步决定下一步做什么,如 this.set

数据结构——30行代码实现栈和模拟递归

本文始发于个人公众号:TechFlow,原创不易,求个关注 栈的定义 原本今天想给大家讲讲快速选择算法的,但是发现一连写了好几篇排序相关了,所以临时改了题目,今天聊点数据结构,来看看经典并且简单的数据结构--栈. 栈这个结构我想大家应该都耳熟能详,尤其是在很多地方将和堆并列在一起,称作"堆栈"就更广为人知了.但其实堆和栈本质上是两种不同的数据结构,我们不能简单地混为一谈.让我们先从比较简单的栈开始. 栈和队列的本质其实都是数组(严格地说是线性表).只不过我们在数组上增加了一些限制,使得

《第一行代码》BroadcastBestPractice中出现的问题及解决方案(关于AlertDialog系统对话框的使用)

该程序为<第一行代码>中的一个小demo,意在学习使用广播实现强制下线功能. 按<第一行代码>中的源码编写ForceOfflineReceiver类,用于接收广播并处理,代码如下: public class ForceOfflineReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { AlertDialog.Builde