看到一篇文章,以爲寫得挺好的,拿過來給你們分享一下,恰好解答了一些困擾個人一些疑惑!!!html
1. 前言
每當被問到Vue數據雙向綁定原理的時候,你們可能都會脫口而出:Vue內部經過Object.defineProperty
方法屬性攔截的方式,把data
對象裏每一個數據的讀寫轉化成getter
/setter
,當數據變化時通知視圖更新。雖然一句話把大概原理歸納了,可是其內部的實現方式仍是值得深究的,本文就以通俗易懂的方式剖析Vue內部雙向綁定原理的實現過程。vue
2. 思路分析
所謂MVVM數據雙向綁定,即主要是:數據變化更新視圖,視圖變化更新數據。以下圖:
node
也就是說:數組
- 輸入框內容變化時,data 中的數據同步變化。即 view => model 的變化。
- data 中的數據變化時,文本節點的內容同步變化。即 model => view 的變化。
要實現這兩個過程,關鍵點在於數據變化如何更新視圖,由於視圖變化更新數據咱們能夠經過事件監聽的方式來實現。因此咱們着重討論數據變化如何更新視圖。緩存
數據變化更新視圖的關鍵點則在於咱們如何知道數據發生了變化,只要知道數據在何時變了,那麼問題就變得迎刃而解,咱們只需在數據變化的時候去通知視圖更新便可。函數
3. 使數據對象變得「可觀測」
數據的每次讀和寫可以被咱們看的見,即咱們可以知道數據何時被讀取了或數據何時被改寫了,咱們將其稱爲數據變的‘可觀測’。post
要將數據變的‘可觀測’,咱們就要藉助前言中提到的Object.defineProperty
方法了,關於該方法,MDN上是這麼介紹的:性能
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。學習
在本文中,咱們就使用這個方法使數據變得「可觀測」。測試
首先,咱們定義一個數據對象car
:
1 let car = { 2 'brand':'BMW', 3 'price':3000 4 }
咱們定義了這個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
的全部屬性都變得可觀測,咱們能夠編寫以下兩個函數:
1 /** 2 * 把一個對象的每一項都轉化成可觀測對象 3 * @param { Object } obj 對象 4 */ 5 function observable (obj) { 6 if (!obj || typeof obj !== 'object') { 7 return; 8 } 9 let keys = Object.keys(obj); 10 keys.forEach((key) =>{ 11 defineReactive(obj,key,obj[key]) 12 }) 13 return obj; 14 } 15 /** 16 * 使一個對象轉化成可觀測對象 17 * @param { Object } obj 對象 18 * @param { String } key 對象的key 19 * @param { Any } val 對象的某個key的值 20 */ 21 function defineReactive (obj,key,val) { 22 Object.defineProperty(obj, key, { 23 get(){ 24 console.log(`${key}屬性被讀取了`); 25 return val; 26 }, 27 set(newVal){ 28 console.log(`${key}屬性被修改了`); 29 val = newVal; 30 } 31 }) 32 }
如今,咱們就能夠這樣定義car
:
1 let car = observable({ 2 'brand':'BMW', 3 'price':3000 4 })
car
的兩個屬性都變得可觀測了。
4. 依賴收集
完成了數據的'可觀測',即咱們知道了數據在何時被讀或寫了,那麼,咱們就能夠在數據被讀或寫的時候通知那些依賴該數據的視圖更新了,爲了方便,咱們須要先將全部依賴收集起來,一旦數據發生變化,就統一通知更新。其實,這就是典型的「發佈訂閱者」模式,數據變化爲「發佈者」,依賴對象爲「訂閱者」。
如今,咱們須要建立一個依賴收集容器,也就是消息訂閱器Dep,用來容納全部的「訂閱者」。訂閱器Dep主要負責收集訂閱者,而後當數據變化的時候後執行對應訂閱者的更新函數。
建立消息訂閱器Dep:
1 class Dep { 2 constructor(){ 3 this.subs = [] 4 }, 5 //增長訂閱者 6 addSub(sub){ 7 this.subs.push(sub); 8 }, 9 //判斷是否增長訂閱者 10 depend () { 11 if (Dep.target) { 12 this.addSub(Dep.target) 13 } 14 }, 15 16 //通知訂閱者更新 17 notify(){ 18 this.subs.forEach((sub) =>{ 19 sub.update() 20 }) 21 } 22 } 23 Dep.target = null;
有了訂閱器,再將defineReactive
函數進行改造一下,向其植入訂閱器:
1 function defineReactive (obj,key,val) { 2 let dep = new Dep(); 3 Object.defineProperty(obj, key, { 4 get(){ 5 dep.depend(); 6 console.log(`${key}屬性被讀取了`); 7 return val; 8 }, 9 set(newVal){ 10 val = newVal; 11 console.log(`${key}屬性被修改了`); 12 dep.notify() //數據變化通知全部訂閱者 13 } 14 }) 15 }
從代碼上看,咱們設計了一個訂閱器Dep類,該類裏面定義了一些屬性和方法,這裏須要特別注意的是它有一個靜態屬性 target
,這是一個全局惟一 的Watcher
,這是一個很是巧妙的設計,由於在同一時間只能有一個全局的 Watcher
被計算,另外它的自身屬性 subs
也是 Watcher
的數組。
咱們將訂閱器Dep添加訂閱者的操做設計在getter
裏面,這是爲了讓Watcher
初始化時進行觸發,所以須要判斷是否要添加訂閱者。在setter
函數裏面,若是數據變化,就會去通知全部訂閱者,訂閱者們就會去執行對應的更新的函數。
到此,訂閱器Dep設計完畢,接下來,咱們設計訂閱者Watcher.
5. 訂閱者Watcher
訂閱者Watcher
在初始化的時候須要將本身添加進訂閱器Dep
中,那該如何添加呢?咱們已經知道監聽器Observer
是在get
函數執行了添加訂閱者Wather
的操做的,因此咱們只要在訂閱者Watcher
初始化的時候出發對應的get
函數去執行添加訂閱者操做便可,那要如何觸發get
的函數,再簡單不過了,只要獲取對應的屬性值就能夠觸發了,核心緣由就是由於咱們使用了Object.defineProperty( )
進行數據監聽。這裏還有一個細節點須要處理,咱們只要在訂閱者Watcher
初始化的時候才須要添加訂閱者,因此須要作一個判斷操做,所以能夠在訂閱器上作一下手腳:在Dep.target
上緩存下訂閱者,添加成功後再將其去掉就能夠了。訂閱者Watcher
的實現以下:
1 class Watcher { 2 constructor(vm,exp,cb){ 3 this.vm = vm; 4 this.exp = exp; 5 this.cb = cb; 6 this.value = this.get(); // 將本身添加到訂閱器的操做 7 }, 8 9 update(){ 10 let value = this.vm.data[this.exp]; 11 let oldVal = this.value; 12 if (value !== oldVal) { 13 this.value = value; 14 this.cb.call(this.vm, value, oldVal); 15 }, 16 get(){ 17 Dep.target = this; // 緩存本身 18 let value = this.vm.data[this.exp] // 強制執行監聽器裏的get函數 19 Dep.target = null; // 釋放本身 20 return value; 21 } 22 }
過程分析:
訂閱者Watcher
是一個 類,在它的構造函數中,定義了一些屬性:
- vm:一個Vue的實例對象;
- exp:是
node
節點的v-model
或v-on:click
等指令的屬性值。如v-model="name"
,exp
就是name
; - cb:是
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
設計完畢。
6. 測試
完成以上工做後,咱們就能夠來真正的測試了。
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 = "難涼熱血" } </script> </body> </html> 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); } } }
效果:
vue數據雙向綁定原理及實現
7. 總結
總結一下:
實現數據的雙向綁定,首先要對數據進行劫持監聽,因此咱們須要設置一個監聽器Observer
,用來監聽全部屬性。若是屬性發上變化了,就須要告訴訂閱者Watcher
看是否須要更新。由於訂閱者是有不少個,因此咱們須要有一個消息訂閱器Dep
來專門收集這些訂閱者,而後在監聽器Observer
和訂閱者Watcher
之間進行統一管理的。
(完)
免責聲明
- 本博客全部文章僅用於學習、研究和交流目的,歡迎非商業性質轉載。
- 博主在此發文(包括但不限於漢字、拼音、拉丁字母)均爲隨意敲擊鍵盤所出,用於檢驗本人電腦鍵盤錄入、屏幕顯示的機械、光電性能,並不表明本人局部或所有贊成、支持或者反對觀點。如須要詳查請直接與鍵盤生產廠商法人表明聯繫。挖井挑水無水錶,不會網購無快遞。
- 博主的文章沒有高度、深度和廣度,只是湊字數。因爲博主的水平不高(實際上是個菜B),不足和錯誤之處在所不免,但願你們可以批評指出。
- 博主是利用讀書、參考、引用、抄襲、複製和粘貼等多種方式打形成本身的文章,請原諒博主成爲一個無恥的文檔搬運工!