一、vue中的双向数据绑定主要使用到了Object.defineProperty(新版的使用Proxy实现的)对Model层的数据进行getter和setter进行劫持,修改Model层数据的时候,在setter中可以知道对那个属性进行修改了,然后修改View的数据。
二、简易版双向数据绑定
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Proxy双向数据绑定大概原理</title> </head> <body> <div id="app"> <input type="text" id="inpt"/> <span id="txt"></span> </div> <script> var inputDom = document.getElementById("inpt"), spanDom = document.getElementById("txt"), data = {} // 更新DOM function notifyToUpdateDOM (newVal) { inputDom.value = newVal spanDom.innerHTML = newVal } var proxyHandler = { get: function(target, property){ return target[property] }, set: function(target, property, value){ target[property] = value notifyToUpdateDOM(value) } } // 创建代理 var dataProxy = new Proxy(data, proxyHandler) // 监听input的input事件 inputDom.addEventListener("input", function(e){ // 设置data中的inputModel属性,会触发set方法的调用 dataProxy.inputModel = e.target.value }) </script> </body> </html>
以上简易代码比较适合Model层没有默认数据的时候,如果Model层的inputModel默认有值为:“双向数绑定”;那么如何在页面初始化完成的时候就把Model层的数据显示到View上呢?因此在进行数据绑定之前,需要把View模板进行编译,和Model层的数据进行关联。
三、实现View数据变化映射到Model数据上,初始化的Model数据映射到View上
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Proxy双向数据绑定大概原理</title> </head> <body> <div id="app"> <input type="text" v-model="text"/>{{text}} </div> <!-- 此次完成了UI到Model的数据绑定,还没有实现Model到UI的绑定({{xxx}}处理没有实现) --> <script> // 参数el:最外层的dom元素,此处案例是app // 参数vm:表示Vue的实例 // 主要处理v-model指令和{{xxx}},把里面的变量和Vue中的进行映射 function CompileTemplate(el, vm){ this.$ele = el this.$vm = vm this.$fragment = null // 保存重新编译之后的dom结构 } CompileTemplate.prototype = { // 修正构造函数的指向 constructor: CompileTemplate, // 返回fragment getDocumentFragment: function(){ if (this.$fragment) { return this.$fragment } else { this.$fragment = this.nodeToFragment() return this.$fragment } }, nodeToFragment: function(){ var node = document.getElementById(this.$ele), fragment = document.createDocumentFragment(), child = null while(child = node.firstChild){ this.compileElement(child) /*如果被插入的节点已经存在于当前文档的文档树中,则那个节点会首先从原先的位置移除, 然后再插入到新的位置;如果你需要保留这个子节点在原先位置的显示,则你需要先用Node.cloneNode 方法复制出一个节点的副本,然后在插入到新位置. */ fragment.appendChild(child) } return fragment }, // 处理节点信息以及绑定事件 compileElement: function(node){ // 匹配{{}} var reg = /{{(.*)}}/g, _this = this // 元素 if (node.nodeType === 1) { var attributes = node.attributes for (var len = attributes.length - 1; len > 0; len--) { // 获取v-model绑定的变量 if (attributes[len].nodeName === 'v-model') { // 获取v-model="txt"中的txt var name = attributes[len].nodeValue // 为input元素绑定input事件,当事件发生时,设置Vue对象中的$data的值 node.addEventListener("input", function(e){ // 设置vue对象中data的text中值 _this.$vm.$data[name] = e.target.value }) // 初始化的时候,需要把Vue对象中的数据赋值给input元素的value node.value = _this.$vm.$data[name]; node.removeAttribute('v-model') } } } // text if (node.nodeType === 3) { if(reg.test(node.nodeValue)) {// 获取v-model绑定的属性名 {{text}} var name = RegExp.$1.trim(); // 获取匹配到的字符串 // 初始化的时候,把vue对象data的数据赋值给{{text}} node.nodeValue = _this.$vm.$data[name]; } } } } function Vue(options){ this.$el = options.el this.$data = options.data // 解析DOM模板,如v-model指令改为input事件,{{xxx}}改为对象中的数据 var fragmentDOM = new CompileTemplate(this.$el, this).getDocumentFragment() // 更新DOM document.getElementById(this.$el).appendChild(fragmentDOM) } var vm = new Vue({ el: "app", data: { text: '双向数据绑定' } }) </script> </body> </html>
四、在案例三的基础上,使用订阅发布模式实现{{xxx}}
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Proxy双向数据绑定大概原理(最终版)</title> </head> <body> <div id="app"> <input type="text" v-model="text"/>{{text}} </div> <!-- 此次实现Model到UI的绑定 --> <script> // 参数el:最外层的dom元素,此处案例是app // 参数vm:表示Vue的实例 // 主要处理v-model指令和{{xxx}},把里面的变量和Vue中的进行映射 function CompileTemplate(el, vm){ this.$ele = el this.$vm = vm this.$fragment = null // 保存重新编译之后的dom结构 } CompileTemplate.prototype = { // 修正构造函数的指向 constructor: CompileTemplate, // 返回fragment getDocumentFragment: function(){ if (this.$fragment) { return this.$fragment } else { this.$fragment = this.nodeToFragment() return this.$fragment } }, nodeToFragment: function(){ var node = document.getElementById(this.$ele), fragment = document.createDocumentFragment(), child = null while(child = node.firstChild){ this.compileElement(child) /*如果被插入的节点已经存在于当前文档的文档树中,则那个节点会首先从原先的位置移除, 然后再插入到新的位置;如果你需要保留这个子节点在原先位置的显示,则你需要先用Node.cloneNode 方法复制出一个节点的副本,然后在插入到新位置. */ fragment.appendChild(child) } return fragment }, // 处理节点信息以及绑定事件 compileElement: function(node){ // 匹配{{}} var reg = /{{(.*)}}/g, _this = this // 元素 if (node.nodeType === 1) { var attributes = node.attributes for (var len = attributes.length - 1; len > 0; len--) { // 获取v-model绑定的变量 if (attributes[len].nodeName === 'v-model') { // 获取v-model="txt"中的txt var name = attributes[len].nodeValue // 为input元素绑定input事件,当事件发生时,设置Vue对象中的$data的值 node.addEventListener("input", function(e){ // 设置vue对象中data的text中值 _this.$vm.$data[name] = e.target.value }) // 初始化的时候,需要把Vue对象中的数据赋值给input元素的value // node.value = _this.$vm.$data[name]; new Watcher(_this.$vm, node, name, "value") node.removeAttribute('v-model') } } } // text if (node.nodeType === 3) { if(reg.test(node.nodeValue)) {// 获取v-model绑定的属性名 {{text}} var name = RegExp.$1.trim(); // 获取匹配到的字符串 // 初始化的时候,把vue对象data的数据赋值给{{text}} // node.nodeValue = _this.$vm.$data[name]; new Watcher(_this.$vm, node, name, "nodeValue") } } } } /* 对model层的数据进行劫持,model改变时,需要通知修改UI的数据,此处使用Proxy处理(兼容性不好), 也可以使用Object.defineProperty处理;Proxy虽然可以动态对被代理的对象进行属性劫持,但是对Model到 UI这条路径还是无法进行双向数据绑定,因为模板先编译了;所以在真正的Vue.js中需要通过this.$set()设置 动态属性,这样才能做到响应式 */ function observe (obj) { var publish = new Publish() var dataProxy = new Proxy(obj, { // 在首次编译模板的时候,创建观察者时,触发vm.$data中属性的get方法 get: function(target, property){ // 把观察者放入发布者中,Publish类的属性 if (Publish.target) { publish.addSubscribe(Publish.target) } return target[property] }, set: function(target, property, value){ if (target[property] === value) { return } target[property] = value // 通知更新UI publish.notify() } }) return dataProxy; } // 发布者 function Publish () { // 保存观察者 this.subscribes = [] } Publish.prototype = { constructor: Publish, // 保存观察者 addSubscribe: function (sub){ // 不存在 if (this.subscribes.indexOf(sub) === -1) { this.subscribes.push(sub) } }, // Model改变了需要更新UI notify: function () { this.subscribes.forEach(function(sub) { sub.update() }) } } // 观察者:绑定Model的数据到UI中对应的DOM节点属性中 // 参数vm:Vue对象的实例 // 参数node:需要进行数据绑定的DOM对象 // 参数name:Vue对象$data的属性名称 // 参数type:DOM元素需要设置数据的属性。如:value(input元素),nodeValue(text元素的内容) function Watcher (vm, node, name, type) { // 在发布者身上绑定一个当前唯一的观察者对象(类似class中的static,属于类属性) Publish.target = this this.vm = vm this.node = node this.name = name this.type = type this.update() // 关键点 Publish.target = null } Watcher.prototype = { constructor: Watcher, // 把Model中的数据绑定到UI中 update: function(){ // 此处会触发vm对象中的get方法 this.node[this.type] = this.vm.$data[this.name] } } function Vue(options){ this.$el = options.el this.$data = observe(options.data) // 解析DOM模板,如v-model指令改为input事件,{{xxx}}改为对象中的数据 var fragmentDOM = new CompileTemplate(this.$el, this).getDocumentFragment() // 更新DOM document.getElementById(this.$el).appendChild(fragmentDOM) } var vm = new Vue({ el: "app", data: { text: '双向数据绑定' } }) </script> </body> </html>
五、参考的文章:https://blog.csdn.net/tangxiujiang/article/details/79594860