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');
})();
实现效果
代码分析
这里先new
创建Model实例
借助观察者模式watch
将dom元素和功能函数传入给watch
watch
将参数函数对象赋值给Model构造函数对象this._listeners
push了这个数组函数。也就是说通过观察模式watch
改变了构造函数Model
对象_listeners
的值。
再调用model的set方法改变其值的。在改变model._value 值的时候同时通过listeners回调函数的方法改变视口的值,这样就做到了模型数据改变视口也改变。
但这样的实现却很别扭,因为我们需要手动监听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,实现起来也很轻松。
虽然绑定的复杂度降低了,这一步依然要依赖我们手动完成,有没有可能把绑定的逻辑从业务代码中彻底解耦呢?
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的自动更新,当然,这样的”框架”还不足以用于生产环境,不过如果它能或多或少地帮助了解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的更新,也是通过框架中的观察者模式实现的。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="div1" bind="model1"></div>
<span bind="hour"></span> : <span bind="minute"></span> : <span bind="second"></span>
</body>
<script type="text/javascript">
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(listeners) {
listeners.call(self, value);
//listeners(value)
});
});
};
//观察模式
Model.prototype.watch = function(listener) {
// 注册监听的回调函数
this._listeners.push(listener);
};
Model.prototype.bind = function (node) {
// 将watch的逻辑和通用的回调函数放到这里
this.watch(function (value) {
node.innerHTML = value;
});
};
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);
}
// 逻辑代码:
/*(function () {
var model = new Model();
model.bind(document.getElementById('div1'));
model.set('hello, this is a divs');
})();*/
new Controller(function (models) {
var model1 = models.model1;
model1.set('this is a div');
});
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);
});
</script>
</html>
mvc原型玩具的基础上继续拓展,可以参考下面的一些方向:
- 实现对input类标签的双向绑定
- 实现对controller所控制的scope的精准控制,这里一个controller就控制了整个dom树
- 实现view层有关dom node隐藏/显示、创建/销毁的逻辑
- 集成virtual dom,增加dom diff的功能,提高渲染效率
- 提供依赖注入功能,实现控制反转
- 对innerHTML的赋值内容进行安全检查,防止恶意注入
- 实现model collection的逻辑,这里每个model只有一个值
- 利用es5中的setter改变set方法的实现,使得对model的修改更加简单
- 在view层中增加对属性和css的控制
10.支持类似AngularJS中双大括号的语法,只绑定部分html......
一个完善的框架要经过无数的提炼和修改,这里只是最初最初的第一步