為Vue3.0做鋪墊之defineProperty & Proxy

atheist1發表於2019-03-28

前言

最近看程式碼發現Object.defineProperty和Proxy這兩個東西出現的比較頻繁,包括Vue原始碼,包括即將到來的vue3,包括一些庫,所以我們這裡將這兩個東西抽取出來做一個規整。
本篇參考MDN。雖然是在炒現飯,更多的是自己養成寫blog的習慣。

Object.defineProperty

語法:

Object.defineProperty(obj,prop,descriptor)

引數:

obj -> 需要劫持的物件
prop -> 劫持的屬性
descriptor -> 屬性描述符

介紹:

obj和prop我們不必多說,就像定義一個物件一樣,鍵值對使用。我們主要重點介紹一下descriptor:

資料描述符

value

表示該屬性對應的值,可以是任意js值,預設為undefined

writable

表示該屬性是否可寫,只有為true的時候該屬性才能被賦值運算子改變,預設為false

存取描述符

get
一個給屬性提供getter的方法,預設為undefined。
當使用obj.xxx時將會呼叫該方法,並將返回值作為取得的值,該方法不會傳參。
this指向的是被定義的物件(obj)
複製程式碼
set
一個給屬性提供setter的方法,預設為undefined。  
當對屬性賦值時會觸發該函式。 
該函式傳入唯一一個引數,就是newVal
複製程式碼

公共描述符

上述兩個描述符是互斥的,如果你定義了get又定義了value,將會報錯,而公共描述符是指可以為該屬性定義公共的描述符

enumerable
只有該屬性enumerable為true時該屬性才會出現在物件的可列舉屬性中。
預設為false。
(可列舉屬性決定了該屬性是否會被for in迴圈找到。 
for in會找到繼承的可列舉屬性,想要找到自身的用Object.keys)
複製程式碼
configurable
只有該屬性的configurable為true時,該屬性才能修改描述符,才能使用delete刪除該屬性值。  
否則刪除會返回false並刪除失敗,預設為false複製程式碼
ps:以上這些描述符不一定指自身屬性,繼承來的屬性也需要考慮在內,所以需要通過Object.create(null)建立一個原型指向null的物件作為繼承物件。
複製程式碼

作用

按照原理來說他是作為一個攔截層一樣,攔截物件屬性的get或者set或者value,比如Vue中的對響應式資料的建立。

Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      // 在編譯模板時會觸發屬性的get方法,將依賴新增到dep裡
      get: function reactiveGetter() {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      // 在設定值時 dep.notify將當前所有依賴觸發更新
      set: function reactiveSetter(newVal) {
        var value = getter ? getter.call(obj) : val;
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        if ("development" !== 'production' && customSetter) {
          customSetter();
        }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
複製程式碼

proxy

語法

let p = new Proxy(target,handler)

引數

target

使用proxy包裝的目標物件(可以是任意型別的物件,包括原生陣列,函式甚至是另一個代理)

handler

一個物件,操作代理時的函式

示例

  1. get
let handler = {
  // 兩個引數 target,name 對應obj 和 key
  // 此處代理了obj的get方法,當呼叫get不存在時返回預設值default
  get: function (target, name) {
    return target[name] ? target[name] : 'default'
  }
}
let obj = {}
let objProxy = new Proxy(obj, handler)
obj.a = 1
obj.b = 2
console.log(objProxy.a,objProxy.b,objProxy.c)
複製程式碼
  1. set
let handler = {
  // 與get不一樣的是,set多了一個value值,是指你新設定的值
  set: function (target, name, value){
    if (name === 'age') {
      if (!Number.isInteger(value)){
        throw new Error('age must be a Number')
      } else if (value > 100) {
        throw new Error('age cant over then 1000')
      }
    }
    target[name] = value
  }
}
let setProxy = new Proxy({}, handler)
setProxy.age = '1'
複製程式碼
  1. 擴充套件建構函式
function extend(sup,base) {
  // 獲取base下原有的descriptor
  var descriptor = Object.getOwnPropertyDescriptor(
    base.prototype,"constructor"
  );
  base.prototype = Object.create(sup.prototype);
  var handler = {
    // 攔截new指令
    construct: function(target, args) {
      // 此時base已經連線到sup原型上了
      var obj = Object.create(base.prototype);
      // apply方法也被攔截了
      this.apply(target,obj,args);
      return obj;
    },
    apply: function(target, that, args) {
      // 這個that指向的是base
      sup.apply(that,args);
      base.apply(that,args);
    }
  };
  var proxy = new Proxy(base,handler);
  descriptor.value = proxy;
  Object.defineProperty(base.prototype, "constructor", descriptor);
  return proxy;
}

var Person = function(name){
  this.name = name
};

var Boy = extend(Person, function(name, age) {
  this.age = age;
});

Boy.prototype.sex = "M";

var Peter = new Boy("Peter", 13);
console.log(Peter.sex);  // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age);  // 13
複製程式碼
  1. 查詢陣列特定物件
var arr = [
  { name: 'Firefox', type: 'browser' },
  { name: 'SeaMonkey', type: 'browser' },
  { name: 'Thunderbird', type: 'mailer' }
]
// 給定陣列,想通過name,下標,type不同方式查詢
let products = new Proxy(arr,{
  get: function(target, key) {
    let types = {}
    let result
    if (Number.isInteger(+key)){
      return target[key]
    } else {
      for (item of target) {
       
        if (item.name === key) {
          result = item
        }
        if (types[item.type]){
          types[item.type].push(item)
        } else {
          types[item.type] = [item]
        }
      }
    }
    if (result) {
      return result
    }
    if (key === 'types') {
      return types
    }
    if (key === 'number') {
      return target.length
    }
    if (key in types) { 
      return types[key]
    }
    
  }
})
複製程式碼

當然Proxy可以劫持的屬性多達13種,我們這裡只是做一個簡單的介紹

對比

proxy是即將到來的vue3代替Object.definePrototype的實現,至於為什麼要用proxy代替我們大概可以闡述出以下幾個觀點:
1.

proxy劫持的是整個物件,而不需要對物件的每一個屬性進行攔截。  
這樣將減少之前對於為了實現整體物件響應式而遞迴對物件每一個屬性進行攔截的操作,大大優化了效能
複製程式碼
對於defineProperty有一個致命的弱點,就是他沒有辦法監聽陣列的變化。  
為了解決這個問題,vue在底層對陣列的方法進行了hack,監聽了每一次陣列特定的操作,併為操作後的陣列實現響應式。
複製程式碼
  methodsToPatch.forEach(function(method) {
    // cache original method
    // 獲取原方法
    var original = arrayProto[method];
    // def方法重新定義arrayMethods的method方法,然後將新的取值方法賦值
    def(arrayMethods, method, function mutator() {
      var args = [],
        len = arguments.length;
      while (len--) args[len] = arguments[len];
      var result = original.apply(this, args);
      var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          // [].push(1),[].unshift(1)
          // arg = [1]
          inserted = args;
          break
        case 'splice':
          // [1,2,3].splice(0,1,1)
          // 第三個引數為插入的值
          inserted = args.slice(2);
          break
      }
      if (inserted) { ob.observeArray(inserted); }
      // 如果是插入操作則對插入的陣列進行響應式觀察
      // 其他操作將手動觸發一次響應收集
      // notify change
      ob.dep.notify();
      return result
    });
  });
複製程式碼

雖然在vue底層對陣列進行了hack,由於defineProperty是沒有辦法進行監聽陣列角標而導致的變化的,無可奈何下只能提供了一個$set方法進行響應收集,而在proxy裡是不存在這個問題的。

let arr = [1,2,3]
let arr1 = new Proxy(arr,{
	set:function(target,key,newVal) {
        target[key] = newVal
		console.log(1)
	}
})
arr1[0] = 2 // 1 arr1 = [2,2,3]
複製程式碼

相容

雖然proxy很好用,但是他存在最大的問題就是相容性,根據MDN所給出的相容來看,對於edge以下的所有ie瀏覽器都不支援(MDN瀏覽器相容)。但是當初Vue剛出來的時候defineProperty實際上也是存在相容問題的,實踐證明優秀的東西是不會被淘汰的。 拒絕IE從你我做起。

後記

如果文章出現問題歡迎小夥伴一起指出,共同進步~
該篇文章收錄到我的github中,有興趣小夥伴可以給個star,近期將對文件庫做一個規整~
最後求一個深圳內推 130985264@qq.com

相關文章