vue全席(1)-實現對普通物件的監測

哈利呦發表於2018-03-27
資料監測是江湖行走必備之良技

自從MVVM框架從前端興起,各種武藝祕技在前端江湖橫飛四起。當今的三大“門派”——angular,vue,react在前端各站鰲頭,究其內門功夫,都有資料繫結(雙向或者單向)一技。angularjs的髒檢查,angular2的zone.js,vue的defineProperty,以及react的單向資料流,都在試圖用最低的成本讓程式猿(媛)能夠快速的飛簷走壁,練出app。

灑家vue資料監測依賴什麼?

vue得益於es5的 Object.defineProperty方法,可以為正常的物件讀取值附加一層攔截。有如火眼金睛,任何物件的一舉一動都窺於目下(斯賊,休敢亂動)。

Object.defineProperty(obj,'keyName',{
    //資料描述符
    configurable:true/false(default)       // 該屬效能否被delete刪除
    enumerable:  true/false(default)       //該屬效能否被列舉
    value:任何JavaScript值(預設undefined)  //該屬性的值
    writable:    true/false(default)       //該屬性值能否被複制運算子改變
    //存取描述符 --> 資料監測主要依靠
    get:Function/undefined(default)        //getter方法,屬性被訪問會觸發
    set:Function/undefined(default)        //setter方法,屬性被賦值會觸發
})

//舉個橘子
let obj = {};
Object.defineProperty(obj,'test',{
    get:function(){
        console.log('我被訪問了,速度返回404');
        return 404;
    },
    set:function(newVal){
        console.log('我被賦值了,^-^,賦的值是:',newVal);
    }
})
console.log(obj.test)//我被訪問了,速度返回404  //404
obj.test = 200;//我被賦值了,^-^,賦的值是 200
複製程式碼

那麼當物件被賦值的時候,會觸發set方法,在set裡比對新值與舊值,然後做相應的處理,不就達到資料監測並處理的目的了嗎?這麼簡單,想想還有點小激動呢。

初級版的資料監測招式

建立一個監測類,能夠監測物件的變動並執行指定的回撥函式

class Survey{
  constructor(obj,callback){
    if(Object.prototype.toString.call(obj) !== '[object Object]')
      console.error('This parameter should be an Object',obj);
    this.callback = callback;
    this.detect(obj);
  }
  detect(target){
    Object.keys(target).forEach(function(key,index,keyArray){
      let value = target[key];  //相當於舊值
      Object.defineProperty(target,key,{
        get:function(){
          return value;
        },
        set:(function(newVal){
          if(value !== newVal) 
          {
            //如果新值是個物件,會再遍歷屬性以監測
            if(Object.prototype.toString.call(newVal) === '[object Object]') 
            {
              this.detect(newVal);
            }
            this.callback(value,newVal);
            value=newVal;      //將新值賦予舊值
          }
        }).bind(this)
      });
    },this);
    if(Object.prototype.toString.call(target[key]) === '[object Object]')
        this.detect(target[key]);
  }
}
//測試 
let  obj={a:1,b:2};
let watcher=new Survey(obj,function(old,newVal){
  console.log(old);
  console.log(newVal);
});
obj.b=3; // 2 3
複製程式碼

是不是感覺很easy,哪裡不會點哪裡? 但是這種設計能應付監測的物件是陣列的情況嗎?答案是不能的。很明顯陣列操作如push或者unshift,以上設計並不能監測到這種舉動。

升級版搞定陣列監測

怎麼能夠偵測到陣列操作帶來的變化呢?陣列的變化來源其方法,如果陣列的方法呼叫附帶著其值的檢測,那不是就可以監測陣列變化了麼,那怎麼讓陣列的方法呼叫時能夠附帶作其它處理呢?能這麼幹的就只能在陣列的原型(prototype)上動心思了。

//陣列的方法就那麼幾個,可以使用改寫其Array.prototype讓每個陣列方法呼叫時,都能檢測操作值的處理
//陣列的方法還有一些
let ARR_METHODS=['push','pop','shift','unshift','splice','sort','reverse','map'];
class  survey_2{
    constructor(obj,callback){
      if(Object.prototype.toString.call(obj) !== '[object Object]' && 
      Object.prototype.toString.call(obj) !== '[object Array]')
        console.error('This parameter should be an Object',obj);
      this.callback=callback;
      this.detect(obj)
    }

    detect(target){
      //如果target是陣列,則重寫其方法
      if(Object.prototype.toString.call(target) === '[object Array]')
        this.overrideArrPrototype(target);
      Object.keys(target).forEach(function(key,index,keyArray){
        let oldVal = target[key];
        Object.defineProperty(target,key,{
          get:function(){
            return oldVal;
          },
          set:(
            function(newVal){
              console.log(newVal);
              if(newVal !== oldVal)
              { 
                let valType=Object.prototype.toString(newVal);
                if( valType === '[object Object]' || 
                    valType === '[object Array]')
                  this.detect(newVal);
                this.callback(oldVal,newVal);
                oldVal = newVal;
              }
            }
          ).bind(this)
        })
        let targetPropType = Object.prototype.toString.call(target[key]);
        if( targetPropType === '[object Object]' || 
            targetPropType === '[object Array]')
          this.detect(target[key]);
      },this);
     
    }

    overrideArrPrototype(array){
      //產生一個陣列原型副本,用於分配給需要重寫的陣列
      let overrideProto=Object.create(Array.prototype);
      let _this=this; 
      //重寫陣列方法
      Object.keys(ARR_METHODS).forEach((key,index,keyArrary)=>{
        let method=ARR_METHODS[key];
        let oldArray=[];
        Object.defineProperty(overrideProto,method,{
          value:function(){
            oldArray=this.slice(0);//儲存舊陣列
            let args=[].slice.apply(arguments);
            Array.prototype[method].apply(this,args);
            _this.detect(this);
            _this.callback(oldArray,this);
          },
          writable:true,
          enumerable:false,
          configurable:true
        })
      });
      //將目標陣列的原型替換成改造後的原型
      array.__proto__=overrideProto;
    }
}

let arr={a:1,b:[1]};
let test =new survey_2(arr,function(oldArr,newArr){
  console.log(oldArr);
  console.log(newArr);
})
arr.b.push(4); //[1]   [1,4]
複製程式碼

這樣就愉快完成了能對物件與陣列進行檢測的一個監測類了,是不是so easy.

總結:vue的資料監測主要運用的是es5的defineProperty特性,對於陣列的檢測則通過重寫其陣列方法來達到檢測的目的。但是進一步,有木有能檢測到哪個地方改變了的方法呢? 下回拆解。

相關文章