web前端mvc库实现
前言
随着前端应用日趋复杂,如今如angular,vue的mvvm框架,基于virtual dom的react等前端库基本成为了各个公司的首选。而以当初最流行的头号大哥backbone为代表的mvc库基本退出了历史舞台。
在现如今人人都说mvvm/react多好,backbone多差的时代。笔者看别人文章,看的时候总是感觉好像有点道理,看完之后如耳边风一般左耳朵进,右耳朵出。
so,痛定思痛之后,笔者定了个小目标,实现了份简易版的backbone库。以设计,实现的角度来对比其它类型库的差异。
ok,废话不多说,上正菜。
思路整理
MVC即将前端应用抽象为Model,View,Control三大模块。View为用户视图,通过浏览器事件接受用户输入。Model为数据模型,他可以随时和后端同步数据。Control则是具体实现View派发的事件,计算并改变Model的数据。
UI可以被抽象为模版+数据,随着用户不断的触发浏览器提供的各种事件,交互不断的进行,Control接受了View指令改变着Model的数据,而View则随着Model的改变做出响应,最终展现在用户面前。
流程图:
模块划分
本篇文章的思路来自于backbone,并屏弃了耦合的后端操作。早期MVC并没有对Control做严格的划分,也许是数据的改变计算并不那么复杂,所以Control功能在View的事件内完成了,也就是说View模块里面耦合了Control的功能。
但近几年flux的action,store的出现,View调用action,具体数据变化计算则在store内部实现,也算是把Control功能从View内部抽象出来了吧。
Event模块
为对象提供对事件的处理和回调,内部实现了观察者(订阅者)模式,如view订阅了model的变化,model变化之后则通知view。
基本方法。
on函数通过event名,在object上绑定callback函数,将回调函数存储在数组里。
off函数移除在object上绑定的callback函数
-
通过event名移除指定callback。如object.off("change", onChange)
通过event名移除所有callback。如object.off("change")
移除所有指定callback。如object.off(null, onChange);
移除所有指定context的callback。如object.off(null, null, context);
清空object所有的callback。如object.off()
trigger函数通过event名,找到object对应的数组,并触发所有数组内回调函数。
注意事项
其所有方法应该支持类似on(name,callback),on('name1 name2 name3',callback), on({name1:callback1,name2:callback2})
这时候则可以抽象内部公用方法。通过递归的方式,on({name1:callback1,name2:callback2})类型的和on('name1 name2 name3',callback)类型,最终转化为最基本的on(name,callback)类型。核心代码如下:
this.eventsApi = function (iteratee, name, callback, context) {
let event;
if (name && typeof name === 'object') {
Object.keys(name).forEach(key=> {
event = this.eventsApi(key, name[key], context);
})
} else if (SEPARATE.test(name)) {
var keys = name.split(SEPARATE);
keys.forEach(key=> {
event = iteratee.call(this,key, name[key], context);
});
} else {
event = iteratee.call(this,name, callback, context);
}
return event;
};
View模块
无状态,实例化的时候可以对应多个model实例,并以观察者的身份观察这些model的变化,通过这些model数据,加上指定的模版渲染dom,展示UI。
销毁的时候注销掉所有model的观察,取消与相关model之间的关联。
实例化的时候通过事件委托注册浏览器事件
实现
_ensureElement,确保View有一层dom包裹,如果this.el这个dom不存在,则通过id,className,tagName创建一个dom并赋值于this.el。
listenTo,将model与view实例关联起来,并收集关联model,存储于listenTo数组内,内部实现则是调用model的on函数
stopListening,view销毁前调用,通过listenTo数组找到关联model,并取消view与这些model之间的观察者关系。
$,将dom的查找定位在 this.$el下
delegateEvents,事件委托,以{'click #toggle-all': 'choose'}为例,为在this.el子节点的id等于toggle-all的dom注册click事件choose函数。核心代码如下:
delegateEvents: function (events) {
var $el = this.$el;
Object.keys(events).forEach(item=> {
var arr = item.split(' ');
if (arr.length === 2) {
var event = arr[0];
var dom = arr[1];
$el.on(event + '.delegateEvents' + this.$id, dom, this[events[item]].bind(this));
}
})
},
undelegateEvents,注销掉通过delegateEvents注册的dom事件
Model模块
Model在backbone里被抽象为object类型的Model和array类型的Collection
承载着应用的状态,可以随时和后端保持同步。
内部实现了对数据变化的监听,一旦发生变化则通知观察者View发生变化。
Model
监听数据的变化,对model的修改,删除之后调用对应的trigger函数,通知订阅了model变化的view。
set函数,改变model数据,并触发change事件
set: function (obj) {
this._changing = true;
this.changed = obj;
this._previousAttributes = Object.assign({}, this.attributes);
this.attributes = Object.assign({}, this.attributes, obj);
const keys = [];
Object.keys(obj).forEach(key=> {
keys.push(key);
this.trigger('change:' + key, this);
}, this);
if (keys.length > 0) {
this.trigger('change', this);
}
this._changing = false;
},
destroy函数触发destroy事件
destroy: function () {
this.stopListening();
this.trigger('destroy', this);
},
Collection
提供数组类型models的push,unshift,pop,shift,remove,reset等功能。push,unshift实际调用add函数,pop,shift实际调用remove函数。
add函数支持任意索引插入指定数组,触发add事件。核心的代码如下:
export const splice = (array, insert, at)=> {
at = Math.max(0, Math.min(array.length, at));
let len = insert.length;
let tail = [];
for (let i = at; i < array.length; i++) {
tail.push(array[i]);
}
for (let i = 0; i < tail.length; i++) {
array[i + at + len] = tail[i];
}
for (let i = 0; i < len; i++) {
array[i + at] = insert[i];
}
return array;
};
remove函数支持删除指定model,触发update事件。
_addReference,调用add方法新增model时,通过观察者模式增加该model与collection之间的关联,model的变化通知collection。核心代码如下:
_addReference: function (model) {
model.on('all',this._onModelEvent,this);
}
_removeReference,调用remove,reset移除model时,取消该model与collection关联。核心代码如下:
_removeReference: function(model) {
if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this);
}
extend
生产环境下需要在保留原生View,Model类的功能情况下做一些业务拓展,这时候需要用到类的继承。
虽然es6支持extend继承,但这边我还是手写了一份。思路则是返回一个构造函数,该函数的原型为新的实例对象props,而props的原型对象则是父函数的原型(有点拗口,自己看代码理解)。
核心代码如下:
export const extend = function (props) {
var parent = this;
var child = function () {
parent.apply(this, arguments);
};
child.prototype = Object.assign(Object.create(parent.prototype), props, { constructor: child });
return child;
};
todomvc效果图
源码
web前端mvc实现
小节
整篇文章基本是围绕着如下2点
view-model,collection-model的观察者模式的实现展开,期间view,model的销毁则取消与之有关联对象的关系,如view销毁时,注销掉与之关联的model的回调函数。
监听数据变化,并通知观察者作出响应,如model变化后触发trigger('change')
好了,文章草草写到这了,多谢各位看官,以上也是纯个人观点,有问题欢迎各位web前端mvc设计指教。