爲Vue3.0作鋪墊之defineProperty & Proxy

前言

最近看代碼發現Object.defineProperty和Proxy這兩個東西出現的比較頻繁,包括Vue源碼,包括即將到來的vue3,包括一些庫,因此咱們這裏將這兩個東西抽取出來作一個規整。
本篇參考MDN。雖然是在炒現飯,更多的是本身養成寫blog的習慣。javascript

Object.defineProperty

語法:

Object.defineProperty(obj,prop,descriptor)vue

參數:

obj -> 須要劫持的對象
prop -> 劫持的屬性
descriptor -> 屬性描述符java

介紹:

obj和prop咱們沒必要多說,就像定義一個對象同樣,鍵值對使用。咱們主要重點介紹一下descriptor:react

數據描述符

value

表示該屬性對應的值,能夠是任意js值,默認爲undefinedgit

writable

表示該屬性是否可寫,只有爲true的時候該屬性才能被賦值運算符改變,默認爲falsegithub

存取描述符

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)bash

參數

target

使用proxy包裝的目標對象(能夠是任意類型的對象,包括原生數組,函數甚至是另外一個代理)app

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

相關文章
相關標籤/搜索