1、vue中的雙向數據綁定主要使用到了Object.defineProperty(新版的使用Proxy實現的)對Model層的數據進行getter和setter進行劫持,修改Model層數據的時候,在setter中能夠知道對那個屬性進行修改了,而後修改View的數據。javascript
2、簡易版雙向數據綁定html
<!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層的數據進行關聯。vue
3、實現View數據變化映射到Model數據上,初始化的Model數據映射到View上java
<!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>
4、在案例三的基礎上,使用訂閱發佈模式實現{{xxx}}node
<!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>
5、參考的文章:https://blog.csdn.net/tangxiujiang/article/details/79594860app