<div id="mobile-list"><h1 v-text="title"></h1><ul><li v-for="item in brands"><b v-text="item.name"></b><span v-show="showRank">Rank: {{item.rank}}</span></li></ul></div>var element = document.querySelector("#mobile-list");var vm = new MVVM(element, {"title" : "Mobile List","showRank": true,"brands" : [{"name": "Apple", "rank": 1},{"name": "Galaxy", "rank": 2},{"name": "OPPO", "rank": 3}]});vm.set("title", "Top 3 Mobile Rank List"); // => <h1>Top 3 Mobile Rank List</h1>模块划分
下文就介绍下这五个模块实现的基本原理(代码只贴重点部分,完整的实现请到我的 Github 翻阅)
1. 编译模块 Compiler
Compiler 的职责主要是对元素的每个节点进行指令的扫描和提取。因为编译和解析的过程会多次遍历整个节点树,所以为了提高编译效率在 MVVM 构造函数内部先将 element 转成一个文档碎片形式的副本 fragment 编译对象是这个文档碎片而不应该是目标元素,待全部节点编译完成后再将文档碎片添加回到原来的真实节点中。
vm.complieElement 实现了对元素所有节点的扫描和指令提取:
vm.complieElement = function(fragment, root) {var node, childNodes = fragment.childNodes;// 扫描子节点for (var i = 0; i < childNodes.length; i++) {node = childNodes[i];if (this.hasDirective(node)) {this.$unCompileNodes.push(node);}// 递归扫描子节点的子节点if (node.childNodes.length) {this.complieElement(node, false);}}// 扫描完成,编译所有含有指令的节点if (root) {this.compileAllNodes();}}vm.compileAllNodes 方法将会对 this.$unCompileNodes 中的每个节点进行编译(将指令信息交给 Parser ),编译完一个节点后就从缓存队列中移除它,同时检查 this.$unCompileNodes.length 当 length === 0 时说明全部编译完成,可以将文档碎片追加到真实节点上了。parser.parseVText = function(node, model) {// 取得 Model 中定义的初始值 var text = this.$model[model];// 更新节点的文本node.textContent = text;// 对应的刷新函数:// updater.updateNodeTextContent(node, text);// 在 watcher 中订阅 model 的变化watcher.watch(model, function(last, old) {node.textContent = last;// updater.updateNodeTextContent(node, text);});}3. 数据订阅模块 Watcherwatcher.watch = function(field, callback, context) {var callbacks = this.$watchCallbacks;if (!Object.hasOwnProperty.call(this.$model, field)) {console.warn("The field: " + field + " does not exist in model!");return;}// 建立缓存回调函数的数组if (!callbacks[field]) {callbacks[field] = [];}// 缓存回调函数callbacks[field].push([callback, context]);}当数据模型的 field 字段发生改变时,Watcher 就会触发缓存数组中订阅了 field 的所有回调。// 拦截 object 的 prop 属性的 get 和 set 方法Object.defineProperty(object, prop, {get: function() {return this.getValue(object, prop);},set: function(newValue) {var oldValue = this.getValue(object, prop);if (newValue !== oldValue) {this.setValue(object, newValue, prop);// 触发变化回调this.triggerChange(prop, newValue, oldValue);}}});然后还有个问题就是数组操作 ( push, shift 等) 该如何监测?所有的 MVVM 框架都是通过重写该数组的原型来实现的:observer.rewriteArrayMethods = function(array) {var self = this;var arrayProto = Array.prototype;var arrayMethods = Object.create(arrayProto);var methods = "push|pop|shift|unshift|splice|sort|reverse".split("|");methods.forEach(function(method) {Object.defineProperty(arrayMethods, method, function() {var i = arguments.length;var original = arrayProto[method];var args = new Array(i);while (i--) {args[i] = arguments[i];}var result = original.apply(this, args);// 触发回调self.triggerChange(this, method);return result;});});array.__proto__ = arrayMethods;}这个实现方式是从 vue 中参考来的,觉得用的很妙,不过数组的 length 属性是不能够被监听到的,所以在 MVVM 中应避免操作 array.lengthupdater.updateNodeTextContent = function(node, text) {node.textContent = text;}v-bind:style 的刷新函数:updater.updateNodeStyle = function(node, propperty, value) {node.style[propperty] = value;}双向数据绑定的实现
其实这个神奇的功能实现原理也很简单,要做的只有两件事:一是数据变化的时候更新表单值,二是反过来表单值变化的时候更新数据,这样数据的值就和表单的值绑在了一起。
数据变化更新表单值利用前面说的 Watcher 模块很容易就可以做到:
watcher.watch(model, function(last, old) {input.value = last;});"表单变化更新数据只需要实时监听表单的值得变化事件并更新数据模型对应字段即可:var model = this.$model;input.addEventListenr("change", function() {model[field] = this.value;});‘其他表单 radio, checkbox 和 select 都是一样的原理。