簡單的數據監聽咱們已經瞭解怎麼作了,但若是屬性也是個對象,咱們但願它也能被監聽呢?顯然咱們須要作循環判斷了。數組
let test = { a:1, b:2, c:{ d:3, e:4, }, }; Object.keys(obj).forEach(key=>{ let val = obj[key]; Object.defineProperty(obj,key,{ get(){ return val; }, set(newVal){ val = newVal; console.log(`你修改了 ${key}`) } }) if(typeof val === 'object'){ let newObj = val; Object.keys(newObj).forEach(key=>{ ... }) } })
把重複操做部分抽離出來變成遞歸函數app
function define(obj,key,val){ Object.defineProperty(obj,key,{ get(){ return val; }, set(newVal){ val = newVal; console.log(`你修改了 ${key}`) } }) } function watch(obj){ Object.keys(obj).forEach(key=>{ let val = obj[key]; define(obj,key,val); if(typeof val === 'object'){ watch(val); } }) } watch(test);
如今咱們已經能夠作到監聽深層對象了,可是若是咱們修改某個屬性爲對象,好比test.a = {a:7,b:9}
,咱們能夠監聽到test.a
的改變,但咱們無法監聽{a:7,b:9}
,因此上面的代碼還須要強化一下。
咱們只須要在set
時,進行一次判斷便可。函數
function define(obj,key,val){ Object.defineProperty(obj,key,{ get(){ return val; }, set(newVal){ val = newVal; console.log(`你修改了 ${key}`) //添加了下面的代碼 if(typeof val === 'object'){ watch(val); } } }) }
到了這一步,一個對象的完整監聽算是創建起來了。
接下來,咱們須要解決一個核心的問題,監聽對象變化,觸發回調函數
這個纔是咱們監聽對象的根本目的,咱們要能添加本身的功能函數進去,而不是寫死的console
優化
若是想要塞功能函數進去,顯然咱們還須要繼續封裝,由於至少咱們要有存儲功能函數的位置,還要有存儲監聽對象的位置,還得提供一個$watch
方法來添加功能函數
因此大概樣子應該是這樣的this
function Observer(obj){ this.data = obj; //存監聽對象 this.func_list = []; //存功能函數 } Observer.prototype.$watch = function(){} //添加功能函數屬於公共方法
正好咱們前面抽離封裝成了函數,只要組合一下便可prototype
function Observer(obj){ this.data = obj; //存監聽對象 this.func_list = []; //存功能函數 watch(this.data); } Observer.prototype.$watch = function(){} //添加功能函數屬於公共方法
接下來咱們考慮一下$watch
函數怎麼寫,正常的監聽大概是這樣的,字面理解就是當屬性age
發生變化時,執行回調函數code
var app = new Observer({ age:25, name:'big white' }) app.$watch('age',function(newVal){ console.log(`年齡已經更新,如今是${newVal}歲`) })
那對咱們內部實現來講,咱們須要維護一個跟屬性相關的回調數組,而且在對應屬性發生變化時,挨個調用這個數組內的函數。server
function Observer(obj){ this.data = obj; //存監聽對象 this.func = {}; //這裏改動了 watch.call(this); //後面解釋爲何使用call } Observer.prototype.$watch = function(key,func){ //添加功能函數屬於公共方法 let arr = this.func[key] || (this.func[key] = []); //沒有對應數組就建立個空的 arr.push(func); } function execute(arr){ //執行功能函數數組 for(let fun of arr){ fun(); } }
那咱們如今把全部的代碼整合一下對象
function judge(val){ //監聽判斷 if(typeof val === 'object'){ new Observer(val); } } function execute(arr,val){ //執行功能函數數組 arr = arr || []; for(let fun of arr){ fun(val); } } function watch(){ //監聽對象 Object.keys(this.data).forEach(key=>{ let val = this.data[key]; define.call(this,key,val); judge(val); }) } function define(key,val){ let Fun = this.func; //拿到回調對象 Object.defineProperty(this.data,key,{ get(){ return val; }, set(newVal){ val = newVal; console.log(`你修改了 ${key}`) judge(val); execute(Fun[key],val); } }) } function Observer(obj){ this.data = obj; //存監聽對象 this.func = {}; //這裏改動了 watch.call(this); //後面解釋爲何使用call } Observer.prototype.$watch = function(key,func){ //添加功能函數屬於公共方法 let arr = this.func[key] || (this.func[key] = []); //沒有對應數組就建立個空的 arr.push(func); }
如今代碼已經跑通,咱們能夠任意添加監聽的回調了,不過有幾個點仍是要單獨說一下。
首先解釋下爲何watch
這個監聽函數要使用call
來調用,緣由很簡單,由於watch
函數內部是要訪問對象實例的,雖然說放到私有方法或者原型上也能訪問到對象實例,可是咱們其實並不但願暴露一個內部實現的方法,因此使用call
既能夠綁定到對象實例,又能避免被暴露出去。define
函數也是同理。
而後第二個須要解釋的是define
函數內的這一句 let Fun = this.func;
。其實最先我寫的時候時候是直接let arr = this.func[key]
,流程一切正常,可是沒法執行回調數組。後來我意識到,define
函數很早就執行了,且只執行一次,那個時候咱們沒有調用過$watch
,理所固然的arr
固然爲undefined,且永遠爲undefined。因此外部必須獲取引用類型的this.func
,即let Fun = this.func;
數組的獲取只能放到set函數
內部,這樣能夠保證,每次execute
咱們都作了一次回調數組的獲取。遞歸
ok,簡單監聽已經實現完畢,咱們調整下代碼結構和名稱,好比this.func
改成this.events
const Observer = (function(){ function judge(val){ //監聽判斷 if(typeof val === 'object'){ new Observer(val); } } function execute(arr,val){ //執行功能函數數組 arr = arr || []; for(let fun of arr){ fun(val); } } function _watch(){ //監聽對象 Object.keys(this.data).forEach(key=>{ let val = this.data[key]; _define.call(this,key,val); judge(val); }) } function _define(key,val){ let Event = this.events; //拿到回調對象 Object.defineProperty(this.data,key,{ get(){ return val; }, set(newVal){ val = newVal; //console.log(`你修改了 ${key}`) judge(val); execute(Event[key],val); } }) } var constructor = function(obj){ this.data = obj; //存監聽對象 this.events = {}; //回調函數對象 _watch.call(this); } constructor.prototype.$watch = function(key,func){ //註冊監聽事件 let arr = this.events[key] || (this.events[key] = []); //沒有對應數組就建立個空的 arr.push(func); } return constructor; })()
上面咱們實現了$watch
,可是也僅僅是監聽簡單屬性,如a
,面對如a.b
這種形式則毫無辦法。
同理,假如a
屬性是個對象,當a.b
發生變化時,也不會觸發a
變化的回調函數。
也就是說咱們的$watch
還停留在簡單的一層對象上,數據的變化沒有辦法傳遞。
經過觀察其實咱們能夠發現,不管是正向監聽a.b
,仍是a.b
的改變要觸發a
的監聽回調函數,逃不過去的東西就是一個層級,或者咱們換個詞path
咱們的judge
函數是監聽深層對象的關鍵
function judge(val){ //監聽判斷 if(typeof val === 'object'){ new Observer(val); } }
顯然,目前雖然完成了監聽,卻沒有和外層對象產生聯繫,當咱們new Observer()
的時候,咱們並不清楚這個新造的對象是根對象仍是子對象,因此新建對象的時候應該把子對象在根對象的路徑path
傳進去。
若是是根對象,那說明沒有path
const Observer = (function(){ ... var constructor = function(obj,path){ this.data = obj; this.events = {}; this.path = path; //將path存在對象內部 _watch.call(this); } return constructor; })()
這樣_define
和_watch
函數內部都能拿到path
,judge
函數也能正確調用
function judge(val,path){ //監聽判斷 if(typeof val === 'object'){ new Observer(val,path); } }
既然有了路徑,當a.b
改變時,咱們除了能夠拿到b
這個屬性名(key
),還能拿到a
這個path
,而咱們註冊事件的屬性名就是a.b
,換句話說當觸發更改時,咱們只要execute(Event[path + '.' + key],val)
便可。
那麼接下來只有一個問題:Event
不是同一個。
解決這個問題也很簡單,讓全部子對象跟根對象共用一個Event
對象便可
const Observer = (function(){ function judge(val,path,Event){ //又多了個參數 Event if(typeof val === 'object'){ new Observer(val,path,Event); } } function _define(key,val){ let Event = this.events; let Path = this.path? this.path+'.'+key : key; Object.defineProperty(this.data,key,{ get(){ return val; }, set(newVal){ if(newVal === val){ return; } val = newVal; judge(val,_this.path,Event); execute(Event[Path],val); } }) } ... var constructor = function(obj,path,Event){ //又多了個參數 Event this.data = obj; this.events = Event?Event:{}; //你們共用根組件的Event對象 this.path = path; //將path存在對象內部 _watch.call(this); } return constructor; })()
以上,咱們就解決了第一個問題,$watch
能夠監聽a.b.c
的值了。
仔細一想,第二個問題其實也已經解決了,由於咱們如今共用一個Event
對象,a.b.c
改變了,咱們只要依次觸發a.b
的回調函數,a
的回調函數便可。而a.b.c
這個path
,已經在咱們手上了,因此只要改造下execute
函數,就能知足全部需求
function execute(Event,path,val){ //參數改變 let path_arr = path.split('.'); path_arr = path_arr.reduce((arr,key,index)=>{ //得到 a a.b a.b.c 數組 let val = arr[index -1]? arr[index-1]+'.'+key : key; arr.push(val); return arr; },[]); for(let i = path_arr.length-1;i>=0;i--){ //倒序調用 先觸發a.b.c 再觸發a.b let funs = Event[path_arr[i]] || []; if(i == path_arr.length-1){ for(let fun of funs){ fun(val); //直接被改變的屬性能夠拿到新值 } }else{ for(let fun of funs){ fun(); } } } }
完整代碼以下:
const Observer = (function(){ function judge(val,path,Event){ //監聽判斷 if(typeof val === 'object'){ new Observer(val,path,Event); } } function execute(Event,path,val){ //執行監聽回調 let path_arr = path.split('.'); path_arr = path_arr.reduce((arr,key,index)=>{ //得到 a a.b a.b.c 數組 let val = arr[index -1]? arr[index-1]+'.'+key : key; arr.push(val); return arr; },[]); for(let i = path_arr.length-1;i>=0;i--){ //倒序調用 先觸發a.b.c 再觸發a.b let funs = Event[path_arr[i]] || []; if(i == path_arr.length-1){ for(let fun of funs){ fun(val); //直接被改變的屬性能夠拿到新值 } }else{ for(let fun of funs){ fun(); } } } } function _watch(){ //監聽對象 Object.keys(this.data).forEach(key=>{ let val = this.data[key]; let Path = this.path? this.path+'.'+key : key; _define.call(this,key,val,Path); judge(val,Path,this.events); }) } function _define(key,val,Path){ let Event = this.events; Object.defineProperty(this.data,key,{ get(){ return val; }, set(newVal){ if(newVal === val){ return; } val = newVal; judge(val,Path,Event); execute(Event,Path,val); } }) } var constructor = function(obj,path,Event){ this.data = obj; this.events = Event?Event:{}; //你們共用根組件的Event對象 this.path = path; //將path存在對象內部 _watch.call(this); } constructor.prototype.$watch = function(key,func){ //註冊監聽事件 let arr = this.events[key] || (this.events[key] = []); //沒有對應數組就建立個空的 arr.push(func); } return constructor; })()
固然,代碼還有繼續優化的空間,不過目前已經能實現了咱們全部的需求,至此,一個監聽對象纔算真正創建起來。