vue數據雙向綁定是經過數據劫持結合發佈-訂閱模式實現的,具體再也不贅述,一圖以蔽之:html
每當被問到Vue數據雙向綁定原理的時候,你們可能都會脫口而出:Vue內部經過Object.defineProperty
方法屬性攔截的方式,把data
對象裏每一個數據的讀寫轉化成getter
/setter
,當數據變化時通知視圖更新。雖然一句話把大概原理歸納了,可是其內部的實現方式仍是值得深究的,本文就以通俗易懂的方式剖析Vue內部雙向綁定原理的實現過程。vue
所謂MVVM數據雙向綁定,即主要是:數據變化更新視圖,視圖變化更新數據。以下圖:node
也就是說:正則表達式
要實現這兩個過程,關鍵點在於數據變化如何更新視圖,由於視圖變化更新數據咱們能夠經過事件監聽的方式來實現。因此咱們着重討論數據變化如何更新視圖。segmentfault
數據變化更新視圖的關鍵點則在於咱們如何知道數據發生了變化,只要知道數據在何時變了,那麼問題就變得迎刃而解,咱們只需在數據變化的時候去通知視圖更新便可。數組
數據的每次讀和寫可以被咱們看的見,即咱們可以知道數據何時被讀取了或數據何時被改寫了,咱們將其稱爲數據變的‘可觀測’。緩存
要將數據變的‘可觀測’,咱們就要藉助前言中提到的Object.defineProperty
方法了,關於該方法,MDN上是這麼介紹的:app
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。函數
在本文中,咱們就使用這個方法使數據變得「可觀測」。測試
首先,咱們定義一個數據對象car
:
let car = { 'brand':'BMW', 'price':3000 }
咱們定義了這個car
的品牌brand
是BMW
,價格price
是3000。如今咱們能夠經過car.brand
和car.price
直接讀寫這個car
對應的屬性值。可是,當這個car
的屬性被讀取或修改時,咱們並不知情。那麼應該如何作纔可以讓car
主動告訴咱們,它的屬性被修改了呢?
接下來,咱們使用Object.defineProperty()
改寫上面的例子:
let car = {} let val = 3000 Object.defineProperty(car, 'price', { get(){ console.log('price屬性被讀取了') return val }, set(newVal){ console.log('price屬性被修改了') val = newVal } })
經過Object.defineProperty()
方法給car
定義了一個price
屬性,並把這個屬性的讀和寫分別使用get()
和set()
進行攔截,每當該屬性進行讀或寫操做的時候就會出發get()
和set()
。以下圖:
能夠看到,car
已經能夠主動告訴咱們它的屬性的讀寫狀況了,這也意味着,這個car
的數據對象已是「可觀測」的了。
爲了把car
的全部屬性都變得可觀測,咱們能夠編寫以下兩個函數:
/** * 把一個對象的每一項都轉化成可觀測對象 * @param { Object } obj 對象 */ function observable (obj) { if (!obj || typeof obj !== 'object') { return; } let keys = Object.keys(obj); keys.forEach((key) =>{ defineReactive(obj,key,obj[key]) }) return obj; } /** * 使一個對象轉化成可觀測對象 * @param { Object } obj 對象 * @param { String } key 對象的key * @param { Any } val 對象的某個key的值 */ function defineReactive (obj,key,val) { Object.defineProperty(obj, key, { get(){ console.log(`${key}屬性被讀取了`); return val; }, set(newVal){ console.log(`${key}屬性被修改了`); val = newVal; } }) }
如今,咱們就能夠這樣定義car
:
let car = observable({ 'brand':'BMW', 'price':3000 })
car
的兩個屬性都變得可觀測了。
完成了數據的'可觀測',即咱們知道了數據在何時被讀或寫了,那麼,咱們就能夠在數據被讀或寫的時候通知那些依賴該數據的視圖更新了,爲了方便,咱們須要先將全部依賴收集起來,一旦數據發生變化,就統一通知更新。其實,這就是典型的「發佈訂閱者」模式,數據變化爲「發佈者」,依賴對象爲「訂閱者」。
如今,咱們須要建立一個依賴收集容器,也就是消息訂閱器Dep,用來容納全部的「訂閱者」。訂閱器Dep主要負責收集訂閱者,而後當數據變化的時候後執行對應訂閱者的更新函數。
建立消息訂閱器Dep:
class Dep { constructor(){ this.subs = [] }, //增長訂閱者 addSub(sub){ this.subs.push(sub); }, //判斷是否增長訂閱者 depend () { if (Dep.target) { this.addSub(Dep.target) } }, //通知訂閱者更新 notify(){ this.subs.forEach((sub) =>{ sub.update() }) } } Dep.target = null;
有了訂閱器,再將defineReactive
函數進行改造一下,向其植入訂閱器:
function defineReactive (obj,key,val) { let dep = new Dep(); Object.defineProperty(obj, key, { get(){ dep.depend(); console.log(`${key}屬性被讀取了`); return val; }, set(newVal){ val = newVal; console.log(`${key}屬性被修改了`); dep.notify() //數據變化通知全部訂閱者 } }) }
從代碼上看,咱們設計了一個訂閱器Dep類,該類裏面定義了一些屬性和方法,這裏須要特別注意的是它有一個靜態屬性 target
,這是一個全局惟一 的Watcher
,這是一個很是巧妙的設計,由於在同一時間只能有一個全局的 Watcher
被計算,另外它的自身屬性 subs
也是 Watcher
的數組。
咱們將訂閱器Dep添加訂閱者的操做設計在getter
裏面,這是爲了讓Watcher
初始化時進行觸發,所以須要判斷是否要添加訂閱者。在setter
函數裏面,若是數據變化,就會去通知全部訂閱者,訂閱者們就會去執行對應的更新的函數。
到此,訂閱器Dep設計完畢,接下來,咱們設計訂閱者Watcher.
訂閱者Watcher
在初始化的時候須要將本身添加進訂閱器Dep
中,那該如何添加呢?咱們已經知道監聽器Observer
是在get
函數執行了添加訂閱者Wather
的操做的,因此咱們只要在訂閱者Watcher
初始化的時候出發對應的get
函數去執行添加訂閱者操做便可,那要如何觸發get
的函數,再簡單不過了,只要獲取對應的屬性值就能夠觸發了,核心緣由就是由於咱們使用了Object.defineProperty( )
進行數據監聽。這裏還有一個細節點須要處理,咱們只要在訂閱者Watcher
初始化的時候才須要添加訂閱者,因此須要作一個判斷操做,所以能夠在訂閱器上作一下手腳:在Dep.target
上緩存下訂閱者,添加成功後再將其去掉就能夠了。訂閱者Watcher
的實現以下:
class Watcher { constructor(vm,exp,cb){ this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); // 將本身添加到訂閱器的操做 }, update(){ let value = this.vm.data[this.exp]; let oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); }, get(){ Dep.target = this; // 緩存本身 let value = this.vm.data[this.exp] // 強制執行監聽器裏的get函數 Dep.target = null; // 釋放本身 return value; } }
過程分析:
訂閱者Watcher
是一個 類,在它的構造函數中,定義了一些屬性:
node
節點的v-model
或v-on:click
等指令的屬性值。如v-model="name"
,exp
就是name
;Watcher
綁定的更新函數;當咱們去實例化一個渲染 watcher
的時候,首先進入 watcher
的構造函數邏輯,就會執行它的 this.get()
方法,進入 get
函數,首先會執行:
Dep.target = this; // 緩存本身
實際上就是把 Dep.target
賦值爲當前的渲染 watcher
,接着又執行了:
let value = this.vm.data[this.exp] // 強制執行監聽器裏的get函數
在這個過程當中會對 vm
上的數據訪問,其實就是爲了觸發數據對象的getter
。
每一個對象值的 getter
都持有一個 dep
,在觸發 getter
的時候會調用 dep.depend()
方法,也就會執行this.addSub(Dep.target)
,即把當前的 watcher
訂閱到這個數據持有的 dep
的 subs
中,這個目的是爲後續數據變化時候能通知到哪些 subs
作準備。
這樣實際上已經完成了一個依賴收集的過程。那麼到這裏就結束了嗎?其實並無,完成依賴收集後,還須要把 Dep.target
恢復成上一個狀態,即:
Dep.target = null; // 釋放本身
由於當前vm
的數據依賴收集已經完成,那麼對應的渲染Dep.target
也須要改變。
而update()
函數是用來當數據發生變化時調用Watcher
自身的更新函數進行更新的操做。先經過let value = this.vm.data[this.exp];
獲取到最新的數據,而後將其與以前get()
得到的舊數據進行比較,若是不同,則調用更新函數cb
進行更新。
至此,簡單的訂閱者Watcher
設計完畢。
完成以上工做後,咱們就能夠來真正的測試了。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1 id="name"></h1> <input type="text"> <input type="button" value="改變data內容" onclick="changeInput()"> <script src="observer.js"></script> <script src="watcher.js"></script> <script> function myVue (data, el, exp) { this.data = data; observable(data); //將數據變的可觀測 el.innerHTML = this.data[exp]; // 初始化模板數據的值 new Watcher(this, exp, function (value) { el.innerHTML = value; }); return this; } var ele = document.querySelector('#name'); var input = document.querySelector('input'); var myVue = new myVue({ name: 'hello world' }, ele, 'name'); //改變輸入框內容 input.oninput = function (e) { myVue.data.name = e.target.value } //改變data內容 function changeInput(){ myVue.data.name = "xfcao" } </script> </body> </html>
可是還有一個細節問題,咱們在賦值的時候是這樣的形式 ' myVue.data.name = "xfcao" ' 而咱們理想的形式是' myVue.name = "xfcao" '爲了實現這樣的形式,咱們須要在new myVue的時候作一個代理處理,讓訪問myVue的屬性代理爲訪問myVue.data的屬性,實現原理仍是使用Object.defineProperty( )對屬性值再包一層:
index.js
function myVue (data, el, exp) { var self = this; this.data = data; Object.keys(data).forEach(function(key) { self.proxyKeys(key); // 綁定代理屬性 }); observe(data); el.innerHTML = this.data[exp]; // 初始化模板數據的值 new Watcher(this, exp, function (value) { el.innerHTML = value; }); return this; } myVue.prototype = { proxyKeys: function (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; } }); } }
observer.js
/** * 把一個對象的每一項都轉化成可觀測對象 * @param { Object } obj 對象 */ function observable (obj) { if (!obj || typeof obj !== 'object') { return; } let keys = Object.keys(obj); keys.forEach((key) =>{ defineReactive(obj,key,obj[key]) }) return obj; } /** * 使一個對象轉化成可觀測對象 * @param { Object } obj 對象 * @param { String } key 對象的key * @param { Any } val 對象的某個key的值 */ function defineReactive (obj,key,val) { let dep = new Dep(); Object.defineProperty(obj, key, { get(){ dep.depend(); console.log(`${key}屬性被讀取了`); return val; }, set(newVal){ val = newVal; console.log(`${key}屬性被修改了`); dep.notify() //數據變化通知全部訂閱者 } }) } class Dep { constructor(){ this.subs = [] } //增長訂閱者 addSub(sub){ this.subs.push(sub); } //判斷是否增長訂閱者 depend () { if (Dep.target) { this.addSub(Dep.target) } } //通知訂閱者更新 notify(){ this.subs.forEach((sub) =>{ sub.update() }) } } Dep.target = null;
watcher.js
class Watcher { constructor(vm,exp,cb){ this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); // 將本身添加到訂閱器的操做 } get(){ Dep.target = this; // 緩存本身 let value = this.vm.data[this.exp] // 強制執行監聽器裏的get函數 Dep.target = null; // 釋放本身 return value; } update(){ let value = this.vm.data[this.exp]; let oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } } }
最後,咱們來實現一個編譯器
編譯器:掃描和解析每一個節點元素,替換模版數據,綁定事件監聽函數,初始化訂閱者
/** * 編譯器 * @param {String} el 根元素 * @param {Object} vm vue對象 */ function Compile(el, vm) { this.el = document.querySelector(el); this.vm = vm; this.fragment = null; this.init(); } Compile.prototype = { constructor: Compile, init: function() { if (this.el) { this.fragment = this.nodeToFragment(this.el); // 移除頁面元素生成文檔碎片 this.compileElement(this.fragment); // 編譯文檔碎片 this.el.appendChild(this.fragment); } else { console.log('DOM Selector is not exist'); } }, /** * 頁面DOM節點轉化成文檔碎片 */ nodeToFragment: function(el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while(child) { fragment.appendChild(child); // append後,原el上的子節點被刪除了,掛載在文檔碎片上 child = el.firstChild; } return fragment; }, /** * 編譯文檔碎片,遍歷到當前是文本節點,則編譯文本節點;若是當前是元素節點,而且存在子節點,則繼續遞歸遍歷 */ compileElement: function(fragment) { var childNodes = fragment.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{\s*((?:.|\n)+?)\s*\}\}/g; var text = node.textContent; if (self.isElementNode(node)) { self.compileAttr(node); } else if (self.isTextNode(node) && reg.test(text)) { // test() 方法用於檢測一個字符串是否匹配某個模式 reg.lastIndex = 0 self.compileText(node, reg.exec(text)[1]); // exec() 方法用於檢索字符串中的正則表達式的匹配 } if (node.childNodes && node.childNodes.length) { // 遞歸遍歷 self.compileElement(node); } }) }, /** * 編譯屬性 */ compileAttr: function(node) { var self = this; var nodeAttrs = node.attributes; Array.prototype.forEach.call(nodeAttrs, function(attr) { var attrName = attr.name; // attrName是DOM屬性名,而exp是vue對象屬性名 if (self.isDirective(attrName)) { // 只對vue自己指令進行操做 var exp = attr.value; // 屬性名或函數名 if (self.isOnDirective(attrName)) { // v-on指令 self.compileOn(node, self.vm, attrName, exp); } else if (self.isBindDirective(attrName)) { // v-bind指令 self.compileBind(node, self.vm, attrName, exp); } else if (self.isModelDirective(attrName)) { // v-model self.compileModel(node, self.vm, attrName, exp); } node.removeAttribute(attrName); } }) }, /** * 編譯v-on指令 */ compileOn: function(node, vm, attrName, exp) { var onReg = /^v-on:|^@/; var eventType = attrName.replace(onReg, ''); var cb = vm.methods[exp]; node.addEventListener(eventType, cb.bind(vm), false); }, /** * 編譯v-bind指令 */ compileBind: function(node, vm, attrName, exp) { var bindReg = /^v-bind:|^:/; var attr = attrName.replace(bindReg, ''); node.setAttribute(attr, vm.data[exp]); new Watcher(vm, exp, function(val) { node.setAttribute(attr, val); }); }, /** * 編譯v-model指令 */ compileModel: function(node, vm, attrName, exp) { var self = this; var modelReg = /^v-model/; var attr = attrName.replace(modelReg, ''); var val = vm.data[exp]; self.updateModel(node, val); // 初始化視圖 new Watcher(vm, exp, function(value) { // 添加一個訂閱者到訂閱器 self.updateModel(node, value); }); node.addEventListener('input', function(e) { // 綁定input事件 var newVal = e.target.value; if (val == newVal) { return; } self.vm.data[exp] = newVal; }, false); }, /** * 屬性是不是vue指令,包括v-xxx:,:xxx,@xxx */ isDirective: function(attrName) { var dirReg = /^v-|^:|^@/; return dirReg.test(attrName); }, /** * 屬性是不是v-on指令 */ isOnDirective: function(attrName) { var onReg = /^v-on:|^@/; return onReg.test(attrName); }, /** * 屬性是不是v-bind指令 */ isBindDirective: function(attrName) { var bindReg = /^v-bind:|^:/; return bindReg.test(attrName); }, /** * 屬性是不是v-model指令 */ isModelDirective: function(attrName) { var modelReg = /^v-model/; return modelReg.test(attrName); }, /** * 編譯文檔碎片節點文本,即對標記替換 */ compileText: function(node, exp) { var self = this; var initText = this.vm.data[exp]; this.updateText(node, initText); // 初始化視圖 new Watcher(this.vm, exp, function(val) { self.updateText(node, val); // node? }); }, /** * 更新文本節點 */ updateText(node, val) { node.textContent = typeof val == 'undefined'? '': val; }, updateModel(node, val, oldVal) { node.value = typeof val == 'undefined'? '': val; }, /** * 判斷元素節點 */ isElementNode(node) { return node.nodeType == 1; }, /** * 判斷文本節點 */ isTextNode(node) { return node.nodeType == 3; } }
參考:http://www.javashuo.com/article/p-zwrvfaia-gr.html