您好,登錄后才能下訂單哦!
這篇文章主要介紹了Vue2.x中如何實現雙向綁定,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
Vue 是利用的 Object.defineProperty()
方法進行的數據劫持,利用 set、get 來檢測數據的讀寫。
https://jsrun.net/RMIKp/embedded/all/light
MVVM 框架主要包含兩個方面,數據變化更新視圖,視圖變化更新數據。
視圖變化更新數據,如果是像 input 這種標簽,可以使用 oninput
事件..
數據變化更新視圖可以使用 Object.definProperty()
的 set
方法可以檢測數據變化,當數據改變就會觸發這個函數,然后更新視圖。
我們知道了如何實現雙向綁定了,首先要對數據進行劫持監聽,所以我們需要設置一個 Observer
函數,用來監聽所有屬性的變化。
如果屬性發生了變化,那就要告訴訂閱者 watcher
看是否需要更新數據,如果訂閱者有多個,則需要一個 Dep 來收集這些訂閱者,然后在監聽器 observer
和 watcher
之間進行統一管理。
還需要一個指令解析器 compile
,對需要監聽的節點和屬性進行掃描和解析。
因此,流程大概是這樣的:
實現一個監聽器 Observer
,用來劫持并監聽所有屬性,如果發生變動,則通知訂閱者。
實現一個訂閱者 Watcher
,當接到屬性變化的通知時,執行對應的函數,然后更新視圖,使用 Dep 來收集這些 Watcher
。
實現一個解析器 Compile
,用于掃描和解析的節點的相關指令,并根據初始化模板以及初始化相應的訂閱器。
Observer
是一個數據監聽器,核心方法是利用 Object.defineProperty()
通過遞歸的方式對所有屬性都添加 setter
、getter
方法進行監聽。
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 添加在 getter
里,是為了讓 Watcher
初始化時觸發,,因此,需要判斷是否需要訂閱者。
在 setter
中,如果有數據發生變化,則通知所有的訂閱者,然后訂閱者就會更新對應的函數。
到此為止,一個比較完整的 Observer
就完成了,接下來開始設計 Watcher.
訂閱者 Watcher
需要在初始化的時候將自己添加到訂閱器 Dep 中,我們已經知道監聽器 Observer
是在 get 時執行的 Watcher 操作,所以只需要在 Watcher
初始化的時候觸發對應的 get 函數去添加對應的訂閱者操作即可。
那給如何觸發 get 呢?因為我們已經設置了 Object.defineProperty()
, 所以只需要獲取對應的屬性值就可以觸發了。
我們只需要在訂閱者 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; }, };
到此為止, 簡單的額 Watcher
設計完畢,然后將 Observer
和 Watcher
關聯起來,就可以實現一個簡單的的雙向綁定了。
因為還沒有設計解析器 Compile
,所以可以先將模板數據寫死。
將代碼轉化為 ES6 構造函數的寫法,預覽試試。
https://jsrun.net/8SIKp/embed...
這段代碼因為沒有實現編譯器而是直接傳入了所綁定的變量,我們只在一個節點上設置一個數據(name
)進行綁定,然后在頁面上進行 new MyVue
,就可以實現雙向綁定了。
并兩秒后進行值得改變,可以看到,頁面也發生了變化。
// 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; } }); }
上面這段代碼的作用是將 this.data
的 key 代理到 this 上,使得我可以方便的使用 this.xx
就可以取到 this.data.xx
。
雖然上面實現了雙向數據綁定,但是整個過程都沒有解析 DOM 節店,而是固定替換的,所以接下來要實現一個解析器來做數據的解析和綁定工作。
解析器 compile
的實現步驟:
解析模板指令,并替換模板數據,初始化視圖。
將模板指定對應的節點綁定對應的更新函數,初始化相應的訂閱器。
為了解析模板,首先需要解析 DOM 數據,然后對含有 DOM 元素上的對應指令進行處理,因此整個 DOM 操作較為頻繁,可以新建一個 fragment 片段,將需要的解析的 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; }
獲取到最外層的節點后,調用 compileElement
函數,對所有的子節點進行判斷,如果節點是文本節點切匹配{{}}這種形式的指令,則進行編譯處理,初始化對應的參數。
然后需要對當前參數生成一個對應的更新函數訂閱器,在數據發生變化時更新對應的 DOM。
這樣就完成了解析、初始化、編譯三個過程了。
接下來改造一個 myVue 就可以使用模板變量進行雙向數據綁定了。
https://jsrun.net/K4IKp/embed...
添加完 compile
之后,一個數據雙向綁定就基本完成了,接下來就是在 Compile
中添加更多指令的解析編譯,比如 v-model
、v-on
、v-bind
等。
添加一個 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; }
上面的 compile
函數是用于遍歷當前 dom
的所有節點屬性,然后判斷屬性是否是指令屬性,如果是在做對應的處理(事件就去監聽事件、數據就去監聽數據..)
在 MyVue
中添加 mounted
方法,在所有操作都做完時執行。
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中如何實現雙向綁定”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。