JavaScript View-Model 模式实战:模型、视图与异步任务的最佳实践
本文介绍一种基于模型、视图和模板的 JavaScript View-Model 代码组织模式。通过分离数据逻辑与UI交互,利用 jQuery.Deferred 处理异步任务,有效消除深层嵌套回调与复杂的条件判断,实现高可维护性的前端代码结构。
引言与代码组织模式概述
构成
这是一个典型的微博列表页面,界面风格类似于新浪微博。上周末,在专注开发的状态下,仅使用5个对象便完成了400行代码的编写,并总结出一套代码组织模式。
实现这一高效开发目标的代码由四个核心要素构成:
| 要素 | 组成 |
|---|---|
| 模型 | Reply、Forward |
| 视图 | CommentEditor、ReplyList、ForwardList |
| 模板 | jQuery.tmpl |
| 异步任务 | jQuery.Deferred |
模型:数据处理的单一职责
模型
模型仅负责数据处理,其职责包括生成、过滤、保存及验证数据,不涉及其他逻辑。
以留言模型为例,在调用保存方法时,它仅接收JSON格式参数,并返回一个异步任务。无论实际处理结果是同步还是异步,对模型而言并不重要。
在此进行验证的原因在于,模型是一个开放对象,也是与服务器交互的最后一道防线。
此外,模型本身不处理验证失败的情况,而是由视图在调用时选择性地处理,例如弹出消息提示或直接忽略并重试。
代码如下:
// 留言模型
var Reply = {
cache : {},
// { sourceid : id,page_size : 10,page_num : 1 }
fetch : function(data) {
return $.post('/ajax/blog/reply/list',data||{}).success(function(resp) {
resp.ok && resp.list &&
$.each(resp.list,function(k,v) {
return Reply.cache[v.id] = v;
});
});
},
// filter('name','king')
filter : function(prop,val) {
return $.grep(this.cache,function(r){ return r[prop] === val });
},
// { content : '想说就说',sourceid : 1001 }
create : function(data) {
// promise
var dfd = $.Deferred(), now = $.now();
if( (now - this.create.timestamp)/1000 < 10 ){
dfd.reject({message:'您发表得太快了,休息一下吧',type:'warn'})
}else if(!data || !data.sourceid){
dfd.reject({message:'非法操作',type:'error'})
}else if(!data.content){
dfd.reject({message:'评论内容不能为空',type:'warn'})
}else{
this.create.timestamp = now;
dfd = $.post('/ajax/blog/reply/create',data);
}
return dfd.promise();
}
};
Reply.create.timestamp = Forward.create.timestamp = $.now() - 1e4; 视图:UI交互与事件绑定
视图
视图对应浏览器页面上的可视部分,每个视图对象包含一个关联的 jQuery 对象作为属性(instance.$el),类似于UI组件中的DOM容器。
视图还具备两个一致的方法:
render 方法用于从模型获取数据,并根据预定义的模板将数据渲染到HTML页面上。
activate 方法用于激活视图,同时绑定相关的DOM事件,所有事件最多委托到$el为止。
在此示例中,CommentEditor是父视图,ReplyList和ForwardList是互斥显示的两个子视图,父子视图之间相互保存引用。
代码如下:
// 回复列表视图
var ReplyList = function(options) {
var opt = this.opt = $.extend({
el : '',
parent : null
},options||{});
this.parent = opt.parent;
this.$el = $(opt.el);
this.activate();
};
ReplyList.prototype = {
render : function() {
var self = this;
Reply.fetch({
page_size : 10, page_num : 1,
sourceid : self.parent.getBlogId()
})
.done(function(data) {
self.$el.html( self.$list = $.tmpl(tpl_reply_list,data) );
});
return self;
},
activate : function() {
this.$el.delegate('a.del',$.proxy(this.del,this))
}
// ...
}
// 评论编辑器视图
CommentEditor.prototype = {
activate : function() {
this.$el.delegate('a.save',$.proxy(this.save,this))
},
save : function() {
var self = this, data = { content : self.getContent(),sourceid : self.getBlogId() };
var task_r = Reply.create(data);
var task_f = Forward.create(data);
// 转发、评论同时进行
$.when(task_r,task_f).then(function(t1,t2) {
// 保存成功,更新视图或关闭
},function(data) {
// 模型验证出错,或远程服务器错误
Sys.info(data.message,data.type);
});
return self;
},
switchView : function(type) {
// 切换子视图
var view_opt = {el:this.$sublist.empty(),parent:this};
if(type === 'reply'){
$label.show();
this.$submit.val('评论');
this.sublist = new ReplyList(view_opt).render();
}else{
$label.hide();
this.$submit.val('转发');
this.sublist = new ForwardList(view_opt).render();
}
}
// ...
} 模板:HTML片段的生成
模板
模板旨在消除繁琐且丑陋的字符串拼接,其作用是能够直接由js对象生成HTML片段。
模板中可以直接遍历对象,套用预定义的函数来格式化数据,例如时间函数nicetime:
代码如下:
// 回复列表模板
var tpl_reply_list = '<ul class="ui-reply-list">\
{{each list}}\
<li data-id="${id}">\
<a class="name" href="/${userid}">${name}:</a>\
<p>${content}</p>\
<time pubdate>${nicetime(timestamp)}</time><a class="del" href="javascript:;">删除</a>\
</li>\
{{/each}}\
</ul>'; 异步任务:jQuery.Deferred的应用
异步任务
Deferred Object 直译为延迟对象,但理解为异步任务更为恰当。异步任务能够消除多层嵌套的回调,使代码书写和阅读更加便利。
从上述模型和视图的代码中可以明显看出,使用异步任务后,代码结构变得更加平面化。
$.Deferred 方法新建的是一个双向任务队列:成功回调函数队列和失败回调函数队列;任务的状态也分为两种:成功和失败,分别可以用isResolved或isRejected来检查任务的当前状态、用resolve或reject修改任务状态。
promise 方法返回任务的只读副本,此副本上不能修改任务状态。毫无疑问,模型应该始终只返回 promise 对象。(注:只读副本仍然可以再次调用 promise 方法再次返回只读副本)
在Reply.create方法中,能够更好地处理自定义的异步任务,而不是直接返回原生的ajax异步任务:
代码如下:
// var dfd = $.Deferred();
$.post('/ajax/blog/reply/create',data)
.success(function(json) {
if(json && json.ok){
dfd.resolve(json.list);
}else{
dfd.reject({message:json.message||'获取失败',type:'error'});
}
})
.fail(function() {
dfd.reject({message:'服务暂时不可用',type:'error'})
}); 总结:拆分的目的与收益
目的及结论
为什么要进行这样的拆分?
收获:提高了可维护性,实现了清晰的API调用、消除了二层以上的if语句、消除了二层以上的回调语句、每个函数控制在二十行之内。
结果:没有过多的重复代码,所有的功能都被打包好了。
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38
