數據監聽進階

嵌套監聽


簡單的數據監聽咱們已經瞭解怎麼作了,但若是屬性也是個對象,咱們但願它也能被監聽呢?顯然咱們須要作循環判斷了。數組

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的實現


若是想要塞功能函數進去,顯然咱們還須要繼續封裝,由於至少咱們要有存儲功能函數的位置,還要有存儲監聽對象的位置,還得提供一個$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函數內部都能拿到pathjudge函數也能正確調用

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;
})()

固然,代碼還有繼續優化的空間,不過目前已經能實現了咱們全部的需求,至此,一個監聽對象纔算真正創建起來。

相關文章
相關標籤/搜索