mvvm模式即model-view-viewmodel模式简称,单项/双向数据绑定的实现,让前端开发者们从繁杂的dom事件中解脱出来,很方便的处理数据和ui之间的联动。
本文将从vue的双向数据绑定入手,剖析mvvm库设计的核心代码与思路。
数据一旦改变则更新数据对应的ui
ui改变则触发事件改变ui对应的数据
通过dom节点的指令获取刷新函数,用来刷新指定的ui。
实现一个桥接的方法,让刷新函数和需要的数据关联起来
监听数据变化,数据改变后通过桥接方法调用刷新函数
ui改变触发对应的dom事件在改变特定的数据
实现observer,重新定义data,为data上每个属性增加setter,getter以监听数据的变化
实现compile,扫描模版template,提取每个dom节点中的指令信息
实现directive,通过指令信息是实例化对应的directive实例,不同类型的directive拥有不同的刷新函数update
实现watcher,让observer的属性监听函数与directive的update函数做一一对应,以实现数据变化后更新视图
MVVM目前划分为observer,compile,directive,watcher四个模块
通过es5规范中的object.defineProperty方式实现对数据的监听
实现思路:
递归遍历data,将data下面所有属性都加上set,get方法,以实现对所有属性的拦截.
注意:对象可能含有数组属性,数组的内置有push,pop,splice等方法改变内部数据.
此时做法是改变数组的原型链,在原型链中增加一层自定义的push,pop,splice方法做拦截,这些方法里面加上我们自己的回调函数,然后在调用原生的push,pop,splice等方法.
具体可以看我上一篇文章js对象监听实现
observer.js代码
export function Observer(obj) {
this.$observe = function(_obj) {
var type = Object.prototype.toString.call(_obj);
if (type == '[object Object]') {
this.$observeObj(_obj);
} else if (type == '[object Array]') {
this.$cloneArray(_obj);
}
};
this.$observeObj = function(obj) {
var t = this;
Object.keys(obj).forEach(function(prop) {
var val = obj[prop];
defineProperty(obj, prop, val);
if (prop != '__observe__') {
t.$observe(val);
}
});
};
this.$cloneArray = function(a_array) {
var ORP = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
var arrayProto = Array.prototype;
var newProto = Object.create(arrayProto);
ORP.forEach(function(prop) {
Object.defineProperty(newProto, prop, {
value: function(newVal) {
var dep = a_array.__observe__;
var re=arrayProto[prop].apply(a_array, arguments);
dep.notify();
return re;
},
enumerable: false,
configurable: true,
writable: true
});
});
a_array.__proto__ = newProto;
};
this.$observe(obj, []);
}
var addObserve = function(val) {
if (!val || typeof val != 'object') {
return;
}
var dep = new Dep();
if (isArray(val)) {
val.__observe__ = dep;
return dep;
}
}
export function defineProperty(obj, prop, val) {
if (prop == '__observe__') {
return;
}
val = val || obj[prop];
var dep = new Dep();
obj.__observe__ = dep;
var childDep = addObserve(val);
Object.defineProperty(obj, prop, {
get: function() {
var target = Dep.target;
if (target) {
dep.addSub(target);
if (childDep) {
childDep.addSub(target);
}
}
return val;
},
set: function(newVal) {
if(newVal!=val){
val = newVal;
dep.notify();
}
}
});
}
实现思路:
1.将模版template上的dom遍历一遍,将其存入文档碎片frag
2.遍历frag,通过attributes获取节点的属性信息,在通过正则表达式过滤属性信息,进而拿到元素节点和文档节点的指令信息
var complieTemplate = function (nodes, model) {
if ((nodes.nodeType == 1 || nodes.nodeType == 11) && !isScript(nodes)) {
paserNode(model, nodes);
if (nodes.hasChildNodes()) {
nodes.childNodes.forEach(node=> {
complieTemplate(node, model);
})
}
}
};
var paserNode = function (model, node) {
var attributes = node.attributes || [];
var direct_array = [];
var scope = {
parentNode: node.parentNode,
nextNode: node.nextElementSibling,
el: node,
model: model,
direct_array: direct_array
};
attributes = toArray(attributes);
var textContent = node.textContent;
var attrs = [];
var vfor;
attributes.forEach(attr => {
var name = attr.name;
if (isDirective(name)) {
if (name == 'v-for') {
vfor = attr;
} else {
attrs.push(attr);
}
removeAttribute(node, name);
}
});
//bug nodeType=3
var textValue = stringParse(textContent);
if (textValue) {
attrs.push({
name: 'v-text',
value: textValue
});
node.textContent = '';
}
if (vfor) {
scope.attrs = attrs;
attrs = [vfor];
}
attrs.forEach(function (attr) {
var name = attr.name;
var val = attr.value;
var directiveType = 'v' + /v-(\w+)/.exec(name)[1];
var Directive = directives[directiveType];
if (Directive) {
direct_array.push(new Directive(val, scope));
}
});
};
var isDirective = function (attr) {
return /v-(\w+)/.test(attr)
};
var isScript = function isScript(el) {
return el.tagName === 'SCRIPT' && (
!el.hasAttribute('type') ||
el.getAttribute('type') === 'text/javascript'
)
}
指令信息如:v-text,v-for,v-model等。
每种指令信息需要的初始化动作以及指令的刷新函数update都可能不一样,所以我们把它抽象出来单独做一个模块。当然也有公用的如公共属性,统一的watcher实例化,unbind.
update函数则具体定义所属指令如何渲染ui
如简单的vtext指令的update函数如下:
vt.update = function (textContent) {
this.el.textContent = textContent;
};
watcher的功能是让directive和observer模块关联起来。
初始化的时候做两件事:
将directive模块的update函数当参数传入,并将其存入自身update属性中
调用getValue,从而获取对象data的特定属性值,进而触发一次之前在observer定义的属性函数的getter方法。
由于在defineProperty函数中定义的dep变量在setter和getter函数里有引用,使dep变量处于闭包状态没有释放,此时在getter方法中通过判断Depend.target的存在,来获取订阅者watcher,通过发布者dep储存起来。
数据的每个属性都有一个唯一的的dep变量,记录着所有订阅者watcher的信息,一旦属性有变化,调用setter函数的时候触发dep.notify(),通知所有已订阅的watcher,进而执行所有与该属性关联的刷新函数,最后更新指定的ui。
watcher 初始化部分代码:
Depend.target = this;
this.value = this.getValue();
Depend.target = null;
observer.js 属性定义代码:
export function defineProperty(obj, prop, val) {
if (prop == '__observe__') {
return;
}
val = val || obj[prop];
var dep = new Dep();
obj.__observe__ = dep;
var childDep = addObserve(val);
Object.defineProperty(obj, prop, {
get: function() {
var target = Dep.target;
if (target) {
dep.addSub(target);
if (childDep) {
childDep.addSub(target);
}
}
return val;
},
set: function(newVal) {
if(newVal!=val){
val = newVal;
dep.notify();
}
}
});
}
简单的流程图如下:
本文基本对mvvm库的需求整理,拆分,以及对拆分模块的逐一实现来达到整体双向绑定功能的实现,当然目前市场上的mvvm库功能绝不止于此,本文只是略举个人认为的核心代码。
如果思路和实现上的问题,也请各位斧正,谢谢阅读!来源:简易mvvm库的设计实现
Web前端开发是Web技术发展中的一个重要组成部分,在传统的前端开发中由于外界因素的影响导致其开发形式呈现出简单化的特点,即以页面为主体来展示界面中的信息。然而随着科学技术的不断进步,Web前端开发形式上变得更为复杂,但是其功能方面也逐渐实现了与用户间的相互交流。
MVC是一种软件架构的思想,即把一个应用的输入、处理、输出流程按照Model、View、Controller的方式进行分离;MVC模式是一个复杂的架构模式,其实现也显得非常复杂,但多种设计模式结合在一起
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!