前端开发模式已经历从MVC、MVP到MVVM的过渡,然而MVX模式本质都是在解决GUI程序的所面临的各种问题,而核心解决的问题是M和V如何通过X来牵线搭桥实现交互。
本文主要介绍MVX不同模式的原理和简单实现。
设定场景
为了介绍,我们来设定一个交互场景:当我们选择某种动物,就在某个区域显示它的叫声。
无模式实现
如果不使用任何模式,我们可以通过如下代码实现:
<select id="animal">
<option value="chick">chick</option>
<option value="hen">hen</option>
<option value="cock">cock</option>
</select>
<div><span id="name"></span>'s voice is <span id="voice"></span>.</div>
document.querySelector('#animal').onchange = function(){
var voices = {
chick : 'bi bi',
hen : 'gu gu',
cock : 'wo wo'
};
var name = this.value, voice = voices[name];
document.querySelector('#name').textContent = name;
document.querySelector('#voice').textContent = voice;
};
这样的实现简单暴力,代码量也很少,但是对于单元测试和代码维护是比较大的。
下面我们就开始MVC、MVP和MVVM的实现。(本文主要是代码实现,原理部分详细解释建议大家阅读http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html)
MVC模式实现
其工作原理实际为:
1.View 传送指令到 Controller
2.Controller 完成业务逻辑后,要求 Model 改变状态
3.Model 将新的数据发送到 View,用户得到反馈
基于这样的原理我们来实现这个业务逻辑:
<select id="animal">
<option value="chick">chick</option>
<option value="hen">hen</option>
<option value="cock">cock</option>
</select>
<div><span id="name"></span>'s voice is <span id="voice"></span>.</div>
var M = {
voices : {
chick : 'bi bi',
hen : 'gu gu',
cock : 'wo wo'
},
name : '',
voice : '',
change : function(name){
this.name = name;
this.voice = this.voices[name];
V.update();//调用V
},
get : function(k){
return this[k];
}
};
var V = {
init : function(){
document.querySelector('#animal').onchange= function(){
C.set(this.value);//调用C
};
},
update : function(){
document.querySelector('#name').textContent = M.get('name');
document.querySelector('#voice').textContent = M.get('voice');
}
};
var C = {
init : function(){
V.init();
},
set : function(name){
M.change(name);//调用M
}
};
C.init();
以上代码很清晰分出M、V和C三个模块,V通过事件通知C控制M的变化,M变化后会调用V进行视图更新,整个流程是单向的。
经过MVC模式的实现,代码分层更清晰,这样对代码的思路清晰,维护也更简单。
然而,MVC模式仍然存在问题:
1.V层很难组件化,界面稍微有一些变动,MVC三层都要联动修改
2.C层单元测试较难,因为C和V耦合较紧,所以想要单独测试C几乎是不可能的事情
MVP模式实现
其工作原理为:
1. 各部分之间的通信,都是双向的。
2. View 与 Model 不发生联系,都通过 Presenter 传递。
3. View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
基于以上原理我们通过代码实现如下:
<select id="animal">
<option value="chick">chick</option>
<option value="hen">hen</option>
<option value="cock">cock</option>
</select>
<div><span id="name"></span>'voice is <span id="voice"></span>.</div>
var M = {
voices : {
chick : 'bi bi',
hen : 'gu gu',
cock : 'wo wo'
},
name : '',
voice : '',
change : function(name){
this.name = name;
this.voice = this.voices[name];
},
get : function(k){
return this[k];
}
};
var V = {
init : function(){
document.querySelector('#animal').onchange = function(){
C.set(this.value);//调用C
};
},
update : function(kv){
for(k in kv){
document.querySelector('#'+k).textContent = kv[k];
}
}
};
var P = {
init : function(){
V.init();//调用V
},
set : function(name){
M.change(name);//调用M
V.update({
name : M.get('name'),
voice : M.get('voice')
});//调用V
}
};
P.init();
从上面的代码来看跟MVC差不太多,区别主要在于M和V没有直接交互,而是通过P来进行完全控制,所以P也被称为控制狂,任何交互上的事情都由很强的控制欲。
MVP较好的解决前面提到的MVC出现的问题,但是也带来了新的问题,也就是P层会越来越庞大,甚至导致维护困难。
MVVM模式实现
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。
基于上面的原理我们代码实现如下:
<div id="content">
<select id="animal" data-bind="change:doChange">
<option value="chick">chick</option>
<option value="hen">hen</option>
<option value="cock">cock</option>
</select>
<div><span data-bind="name"></span>'voice is <span data-bind="voice"></span>.</div>
</div>
var M = {
voices : {
chick : 'bi bi',
hen : 'gu gu',
cock : 'wo wo',
},
name : '',
voice : ''
doChang : function(){
M.name = this.value;
M.voice = this.voices[obj.name];
}
};
var V = {
bindEvent : function(el, evt, func){
el['on'+evt] = M[func]();
},
updateText : function(el, text){
el.textContent = text;
}
};
var VM = {
init : function(rootSelector){
this.observer();
this.compiler(rootSelector);
},
paths : {},
observer : function(){
var _this = this;
for(var k in M){
(function(k){
var name = k, value = M[k];
Object.defineProperty(M, name, {
get : function(){
return value;
},
set : function(v){
value = v;
V.updateText(_this.paths[name], v);
}
});
})(k);
}
},
compiler : function(rootSelector){
var binds = document.querySelector(rootSelector).querySelectorAll('[data-bind]');
for(var i=0, len=binds.length;i<len;i++){
var el = binds[i];
var directive = el.getAttribute('data-bind');
if(directive.indexOf(':')>-1){//事件指令
var directives = directive.split(':');
var evt = directives[0], func = directives[1];
V.bindEvent(el, evt, func);
}else{//文本指令
this.paths[directive] = el;
V.updateText(el, M[directive]);
}
}
}
};
VM.init('#content');
上面的代码实际只是实现了简单双向绑定,下次在介绍Agile.vm框架的时候再做详解。
从上面的代码看,我们在调用init方法的时候传递了一个选择器,首先对M层数据进行变化检测(observer方法),然后MV再以这个选择器对应的dom作为根节点进行扫描(compiler方法),获取所有VM指令(data-bind)的节点,并缓存起来。
经过这样的设计,M和V已经没有必然关联,只要符合data-bind规则的指令都会被解析,V层的结构无论怎么变化,只要需要有数据绑定的地方加上指令即可。
内部执行效果为,当select的change事件触发,会调用M改变其name和voice的值,VM层的observer检测到M数据变化调用V的updateText来修改文本内容的变化,从而实现数据的联动。
MVVM模式大大提高代码的可维护性,甚至我们使用Sprite开发的应用切换到HTML5也仅仅只需把V层改变即可,同时也简化了测试,只要确保MVVM框架的准确性,也就是VM的绑定关系准确,整个逻辑就是正确的。
但缺点也很明显:数据绑定的是通过指令写在View的模版中的,所以调试比较复杂,需要对框架比较了解才能调试。
总结
MVC、MVP和MVVM模式之间并不是升级关系,各自有自己的使用场景MVVM更适合页面框架层面的搭建使用,特别是移动端APP的开发,目前热门的React Native和Weex都是采用MVVM模式开发;MVP适合复杂模板数据控制使用;MVC则适合小业务功能的实现。
更多的使用场景需要我们不断的总结,新的模式也需要在实践中创造。