最近學習vue源碼,在學習關於數據雙向綁定的時候。看了好幾遍,仍是有不少點不太理解。部門的大神建議本身按照思想模仿的寫一個,體會會深不少。因而照作了,還真是神清氣爽。這篇文章記錄本身在寫demo時碰見的思路和問題,往後回憶複習起來也方便。vue
vue的雙向數據綁定是區分普通對象和數組的。數組的比較複雜,下篇再介紹。今天介紹vue對於對象數據的雙向綁定。vue是經過數據劫持的方式來實現雙向數據綁定的。數據劫持的核心就是object.defineProperty().簡單介紹下這個方法。這個方法是es5定義的,通過該方法定義的對象屬性會變成訪問器屬性。如下是一個簡單的例子:數組
function Observer(obj,key,value){
if(Object.prototype.toString.call(value)=='[object Object]'){ Object.keys(value).forEach(function(key){ arguments.callee(value,key,value[key]); }) } Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:function(){ }, set:function(){ } }) }
訪問器屬性的最大特色即是內部能夠指定get、set方法。在對屬性進行值訪問的時候會調用定義的get方法,對屬性進行賦值的時候會調用set方法。數據結構
接着說雙向數據綁定。雙向數據綁定分爲如下三個部分:函數
Observer:負責數據劫持,把全部的屬性轉換成訪問器屬性,達到對數據進行觀測的目的。須要對數據進行遞歸觀測,由於數據的屬性值還有多是對象學習
Watcher:數據的觀察者,在數據發生變化以後執行的相應的回調函數,須要對數據進行遞歸watch,由於數據的屬性值還有多是對象this
Dep(Dependency):顧名思義,是Observer和Watcher的鏈接。如何鏈接呢?每個observer會建立一個Dep實例,實例在get數據的時候爲數據收集watcher,在set的時候執行watcher內的回調方法。es5
以上是vue中的作法。我本身實現demo的時候就是根據這個思路進行實現的。spa
先是Observer,遞歸將屬性設置爲訪問器屬性,代碼以下:prototype
function Observer(obj,key,value){
if(Object.prototype.toString.call(value)=='[object Object]'){ Object.keys(value).forEach(function(key){ new arguments.callee(value,key,value[key]); }) } Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:function(){ return value; }, set:function(newVal){ if(value===newVal)return;
value = newVal; } }) }
先判斷屬性值value是否是對象,若是是,還須要對對象進行遞歸調用,觀測數據雙向綁定
接着是Watcher,目的在於在數據發生變化的時候執行相應的回調函數
function Watcher(data,k,v,fn){ if(Object.prototype.toString.call(data)==='[object Object]'){ Object.keys(v).forEach(function(key){ new arguments.callee(v,key,v[key],fn); }) } this.fn = fn; data[k]; }
也是先判斷是否是對象,是的話遞歸調用觀察屬性值。如何讓watcher和observer產生聯繫呢?
observer中對全部的屬性設置成了訪問器屬性,因此若是咱們在watcher中調用屬性,求屬性的值就會調用到屬性的get方法。
既然observer和watcher能夠在get方法內產生鏈接,那麼是否是能夠在get的時候收集不一樣的watcher,而後在set函數唄調用的時候執行這些watcher中的方法。這樣就須要在observer中引入一個對象,在get函數內收集watcher,在set函數內遍歷執行watcher的回調方法,已達到動態響應的目的。
這個對象就是Dep。每一個observer內都會實例化一個Dep對象,用於收集watcher和用於執行watcher,根據這個思路,能夠獲得如下的代碼:
function Dep(){ var sub=[]; this.addSub=function(watcher){ this.sub.push(watcher); }; this.notify=function(){ this.sub.forEach(function(watcher){ watcher.fn(); }) } }
根據思路,Dep當中須要一個存儲watcher的數據結構,從添加和遍歷的角度選擇,數組比較合適。而後是須要一個添加watcher的方法,在就是須要一個遍歷watcher的方法。進而,得出了以上的代碼。
如今三個組件都已經有了,那他們之間怎麼協調工做呢?按照以前的思路,和咱們如今有的代碼。在observer中加入Dep收集和執行依賴的代碼。就發現,存在怎麼在get中獲得watcher的實例的問題。既然Dep自己做爲watcher和observer的鏈接橋樑,那這個事情就讓Dep作吧。此時須要作的事情是,須要一個變量,在Watcher中收集watcher實例,在get中將Watcher實例放入Dep實例的數組中,以便於set中使用。
思考這個變量的功能,他不能出如今構造函數和原型鏈中,這樣watcher的變化會實時的提如今每一個實例上。那麼只有在構造函數自己這個函數對象定義這個變量比較合適了。函數對象上的變量不會經過new操做符影響到全部實例,又能完成存儲watcher的功能。
先在watcher中收集,那麼watcher中的代碼以下:
function Watcher(data,k,v,fn){ if(Object.prototype.toString.call(data)==='[object Object]'){ Object.keys(v).forEach(function(key){ new arguments.callee(v,key,v[key],fn); }) } this.fn = fn; Dep.target = this;// data[k]; Dep.target=null;// }
在watcher調用屬性求值以前,將watcher保存到Dep.target變量中,在求值以後(get中Dep實例收集了以後)將該值置爲null,這樣就不會在別的屬性求值的時候影響到別的屬性。
已經在Watcher中收集到了watcher實例,那麼observer中如何使用呢。看以下代碼:
function Observer(obj,key,value){ var dep = new Dep(); if(Object.prototype.toString.call(value)=='[object Object]'){ Object.keys(value).forEach(function(key){ new arguments.callee(value,key,value[key]); }) } Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:function(){ if(Dep.target){//存儲依賴 dep.addSub(Dep.target);// }// return value; }, set:function(newVal){ if(value===newVal)return; value = newVal; dep.notify();//執行依賴 } }) }
到這一步,咱們基本上對象的雙向綁定已經完成了。全部的功能都已經實現了。
在我本身運行調試的時候發現一個問題。若是set的值又是一個對象,那麼對象的屬性改變將沒法獲得監控。因此,在set中加上以上代碼就完整了:
if(Object.prototype.toString.call(value) ==='[object Object]'){ Object.keys(value).forEach(function(key){ new Observer(value,key,value[key]); new Watcher(value,key,value[key],function(v,key){ console.log('你修改了數據'); // document.getElementById('dd').innerHTML=v.key; }); })
}
以上就是我根據vue實例實現的關於對象的實時監聽的小demo了。
最後,我想了想。爲何vue要分這三個部分作呢?細想起來watcher主要的功能也就是set的時候的回調函數。若是沒學習過vue的源碼,應該就是直接把函數傳入observer作回調函數就是了。這樣watcher省了,dep也不用了。
可是又一想,同一個屬性可能同時有幾個watcher,就會要執行多個回調函數。那麼就有了watcher和dep的必要了。