mvvm模式即model-view-viewmodel模式简称,单项/双向数据绑定的实现,让前端开发者们从繁杂的dom事件中解脱出来,很方便的处理数据和ui之间的联动。
本文将从vue的双向数据绑定入手,剖析mvvm库设计的核心代码与思路。javascript
数据一旦改变则更新数据对应的ui前端
ui改变则触发事件改变ui对应的数据vue
经过dom节点的指令获取刷新函数,用来刷新指定的ui。java
实现一个桥接的方法,让刷新函数和须要的数据关联起来node
监听数据变化,数据改变后经过桥接方法调用刷新函数git
ui改变触发对应的dom事件在改变特定的数据github
实现observer,从新定义data,为data上每一个属性增长setter,getter以监听数据的变化正则表达式
实现compile,扫描模版template,提取每一个dom节点中的指令信息segmentfault
实现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库功能毫不止于此,本文只是略举我的认为的核心代码。若是思路和实现上的问题,也请各位斧正,谢谢阅读!