在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Vue 是利用的 https://jsrun.net/RMIKp/embedded/all/light MVVM 框架主要包含两个方面,数据变化更新视图,视图变化更新数据。 视图变化更新数据,如果是像 input 这种标签,可以使用 数据变化更新视图可以使用 1、实现过程我们知道了如何实现双向绑定了,首先要对数据进行劫持监听,所以我们需要设置一个 如果属性发生了变化,那就要告诉订阅者 还需要一个指令解析器 因此,流程大概是这样的:
2、显示一个 Observer
var library = { book1: { name: "", }, book2: "", }; observe(library); library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南” library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍” // 为数据添加检测 function defineReactive(data, key, val) { observe(val); // 递归遍历所有子属性 let dep = new Dep(); // 新建一个dep Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { // 判断是否需要添加订阅者,仅第一次需要添加,之后就不用了,详细看Watcher函数 dep.addSub(Dep.target); // 添加一个订阅者 } return val; }, set: function(newVal) { if (val == newVal) return; // 如果值未发生改变就return val = newVal; console.log( "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”" ); dep.notify(); // 如果数据发生变化,就通知所有的订阅者。 }, }); } // 监听对象的所有属性 function observe(data) { if (!data || typeof data !== "object") { return; // 如果不是对象就return } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); } // Dep 负责收集订阅者,当属性发生变化时,触发更新函数。 function Dep() { this.subs = {}; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach((sub) => sub.update()); }, }; 思路分析中,需要有一个可以容纳订阅者消息订阅器 Dep,用于收集订阅者,在属性发生变化时执行对应的更新函数。 从代码上看,将订阅器 Dep 添加在 在 到此为止,一个比较完整的 3、实现 Watcher订阅者 那给如何触发 get 呢?因为我们已经设置了 我们只需要在订阅者 Watcher 初始化的时候,在 Dep.target 上缓存下订阅者,添加成功之后在将其去掉就可以了。 function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); // 将自己添加到订阅器的操作 } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; // 缓存自己,用于判断是否添加watcher。 var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; }, }; 到此为止, 简单的额 因为还没有设计解析器 将代码转化为 ES6 构造函数的写法,预览试试。 https://jsrun.net/8SIKp/embed... 这段代码因为没有实现编译器而是直接传入了所绑定的变量,我们只在一个节点上设置一个数据( 并两秒后进行值得改变,可以看到,页面也发生了变化。 // MyVue proxyKeys(key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function proxyGetter() { return self.data[key]; }, set: function proxySetter(newVal) { self.data[key] = newVal; } }); } 上面这段代码的作用是将 4、实现 Compile虽然上面实现了双向数据绑定,但是整个过程都没有解析 DOM 节店,而是固定替换的,所以接下来要实现一个解析器来做数据的解析和绑定工作。 解析器
为了解析模板,首先需要解析 DOM 数据,然后对含有 DOM 元素上的对应指令进行处理,因此整个 DOM 操作较为频繁,可以新建一个 fragment 片段,将需要的解析的 function nodeToFragment(el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 将Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild; } return fragment; } 接下来需要遍历各个节点,对含有相关指令和模板语法的节点进行特殊处理,先进行最简单模板语法处理,使用正则解析“{{变量}}”这种形式的语法。 function compileElement (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{(.*)\}\}/; // 匹配{{xx}} var text = node.textContent; if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令 self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); // 继续递归遍历子节点 } }); }, function compileText (node, exp) { var self = this; var initText = this.vm[exp]; updateText(node, initText); // 将初始化的数据初始化到视图中 new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数 self.updateText(node, value); }); }, function updateText (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; } 获取到最外层的节点后,调用 然后需要对当前参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。 这样就完成了解析、初始化、编译三个过程了。 接下来改造一个 myVue 就可以使用模板变量进行双向数据绑定了。 https://jsrun.net/K4IKp/embed... 5、添加解析事件添加完 添加一个 v-model 和 v-on 解析: function compile(node) { var nodeAttrs = node.attributes; var self = this; Array.prototype.forEach.call(nodeAttrs, function(attr) { var attrName = attr.name; if (isDirective(attrName)) { var exp = attr.value; var dir = attrName.substring(2); if (isEventDirective(dir)) { // 事件指令 self.compileEvent(node, self.vm, exp, dir); } else { // v-model 指令 self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); // 解析完毕,移除属性 } }); } // v-指令解析 function isDirective(attr) { return attr.indexOf("v-") == 0; } // on: 指令解析 function isEventDirective(dir) { return dir.indexOf("on:") === 0; } 上面的 6、完整版 myVue在 class MyVue { constructor(options) { var self = this; this.data = options.data; this.methods = options.methods; Object.keys(this.data).forEach(function(key) { self.proxyKeys(key); }); observe(this.data); new Compile(options.el, this); options.mounted.call(this); // 所有事情处理好后执行mounted函数 } proxyKeys(key) { // 将this.data属性代理到this上 var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function getter() { return self.data[key]; }, set: function setter(newVal) { self.data[key] = newVal; }, }); } } 然后就可以测试使用了。 https://jsrun.net/Y4IKp/embed... 总结一下流程,回头在哪看一遍这个图,是不是清楚很多了。 可以查看的代码地址:Vue2.x 的双向绑定原理及实现 到此这篇关于 |
请发表评论