初識Proxy

kerin發表於2021-12-03

最近在複習Vue,不可避免的會接觸到vue3,所以也不可避免的會思考這些問題

  1. vue3實現響應式為什麼要使用proxy替換Object.defineProperty?Proxy對比Object.defineProperty有啥優缺點?
  2. 怎麼通過Proxy實現響應式?

本文會回答這兩個問題,通過這些問題探討Proxy,以及Proxy在日常開發中的應用場景。

認識Proxy

Proxy意思翻譯過來就是代理,外界對目標物件的訪問都會被Proxy攔截,從而可以實現基本操作的攔截和自定義。

用法

let proxy = new Proxy(target,handler)
  • target: 所要攔截的目標物件
  • handler: handler是一個包含你要攔截和處理的物件,當物件被代理時,handler通過捕捉器(trap)實現對各種行為的攔截

目前proxy支援13種行為的攔截

handler方法何時觸發
get讀取屬性
set寫入屬性
hasin操作符
deletePropertydelete操作符
apply函式呼叫
constructnew操作符
getPrototypeOfObject.getPrototypeOf
setPrototypeOfObject.setPrototypeOf
isExtensibleObject.isExtensible
preventExtensionsObject.preventExtensions
definePropertyObject.defineProperty,
Object.defineProperties
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor,
for...in,
Object.keys/values/entries
ownKeysObject.getOwnPropertyNames,
Object.getOwnPropertySymbols,
for...in,
Object.keys/values/entries

Reflect

reflect翻譯過來是對映的意思,在MDN上是這樣定義的

Reflect 是一個內建的物件,它提供攔截 JavaScript 操作的方法。

每個可用的代理捕捉器(trap)都有一個對應的同名Reflect函式,並能產生相同的行為。

let obj = {
    a: 10,
    name: 'oyc'
}

let newTarget = new Proxy(obj, {
    set(target, key, val) {
        console.log(`Set ${key}=${val} `);
    }
})

// newTarget.a = 20;  //Set a=20
// Reflect.set(newTarget, 'a', 20); //Set a=20

newTarget.name = 'oyq'; //Set name=oyq

Reflect.set(newTarget, 'name', 'oyq'); //Set name=oyq

從這可以看出,Reflect和trap表現出來的行為是相同的。所以當你為如何去觸發trap而煩惱的時候,也許這個Reflect可以幫到你。

兩個問題

大致學習完proxy的內容後,再來嘗試解答下頁頭提到的兩個個問題。

vue3實現響應式為什麼要使用proxy替換Object.defineProperty?優缺點?

優點

  • 效能更好,Object.defineProperty只能劫持物件的屬性,所以如果有巢狀物件,初始化時需要遍歷data中的每個屬性,在vue3中,proxy可以代理物件,不需要像vue2那樣對屬性進行遍歷的操作
//vue2
function reactive(obj) {
    // 遍歷物件
    for (const item in obj) {
        if (obj[item] && typeof obj[item] == 'object') {
            // 遞迴,又重新遍歷
            reactive(obj[item])
        } else {
            defineReactive(obj, item, obj[item])
        }
    }
}

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        //set,get操作
    })
}


//vue3
let newTarget = new Proxy(obj, {
  // set,get操作
})
  • 自動代理新增屬性,陣列,Object.defineProperty的實現是對屬性進行劫持,所以當新增屬性時,需要重新遍歷,對新增的重新進行劫持。所以需要vue2對新增的屬性,以及陣列進行 $set 才能保證屬性是響應式的,這個過程是手動的。
let obj = {
  a: 10,
  name: 'oyc'
}
//vue2
this.$set(this.obj, 'age', 18); //每次新增都需要進行這個操作


//vue3
//自動代理
let newTarget = new Proxy(obj, {
  get(target, key) {
    return Reflect.get(target, key);
  },
  set(target, key, val) {
    return Reflect.set(target, key, val);
  }
})
  • Proxy支援13中攔截操作,Object.defineProperty無法比擬
  • Proxy是新標準,後續也會優先優化,Object.defineProperty的setter,getter後續應該優化的優先順序較低

缺點

顯而易見的,Proxy的相容性相較於Object.defineProperty,是較低的,不支援IE瀏覽器。不過以目前的市場份額來看,IE瀏覽器的市場份額也不多,目前微軟也將ie換成了chrome核心的edge,所以激進點的專案是完全可以使用proxy的。

image-20211202224940291.png

怎麼通過Proxy實現響應式?

let obj1 = {
    a: 10,
    name: 'John',
    list: [1, 2, 3],
    obj2: {
        obj3: 'oo',
        obj4: {
            name: 'oyc'
        }
    }
}

// 判斷是否是物件
const isObj = (obj) => typeof obj === 'object' && obj !== null;

const render = (key, val) => {
    console.log(`Render ${key}=${val}`);
}

function reactive(obj) {
    if (!isObj(obj)) {
        return obj;
    }
    const handler = {
        get(target, key) {
            // 對巢狀物件遍歷
            if (isObj(target[key])) {
                // 遞迴
                return reactive(target[key]);
            }
            return Reflect.get(target, key);
        },
        set(target, key, val) {
            // 渲染
            render(key, val);
            return Reflect.set(target, key, val);
        }
    }
    const targetProxyObj = new Proxy(obj, handler);
    return targetProxyObj
}

let myObj = reactive(obj1);

myObj.a = 20; // Render a=20
myObj.b = 30; //新增屬性 Render b=30
myObj.list = [1, 2, 5, 6]; //修改陣列 //Render list=1,2,5,6
myObj.obj2.obj4.name = 'oyq'; //修改巢狀物件 //Render name=oyq

Proxy應用場景

  • 寫入預設值,日常開發經常碰到 ReferenceError: xxx is not defined 這種錯誤,這裡我們可以代理,當屬性不存在時,不報錯,而設定一個預設值
let obj = {
    name: 'oyq'
}

let proxyObj = new Proxy(obj, {
    get(target, key) {
        if (Reflect.has(target, key)) {
            return target[key];
        } else {
            return 'OYC';
        }
    },
})

console.log(proxyObj.age);//OYC
  • 用Proxy來包裝fetch,讓fetch更易用
let handlers = {
  get (target, property) {
    if (!target.init) {
      // 初始化物件
      ['GET', 'POST'].forEach(method => {
        target[method] = (url, params = {}) => {
          return fetch(url, {
            headers: {
              'content-type': 'application/json'
            },
            mode: 'cors',
            credentials: 'same-origin',
            method,
            ...params
          }).then(response => response.json())
        }
      })
    }

    return target[property]
  }
}
let API = new Proxy({}, handlers)

await API.GET('XXX')
await API.POST('XXX', {
  body: JSON.stringify({name: 1})
})
  • 檢驗表單
let formData = {
    name: '',
    age: ''
}

let proxyObj = new Proxy(formData, {
    set(target, key, val) {

        if (key === 'age' && typeof val !== 'number') {
            console.log('age must be number');
        }

        if (key === 'name' && typeof val !== 'string') {
            console.log('name must be string');
        }
    }
})

proxyObj.age = 'oyc'; //age must be number
proxyObj.name = 18; //name must be string
  • 負索引陣列
let arr = [1, 2, 3, 4, 5]

let proxyArray = new Proxy(arr, {
    get(target, key) {
        const index = key < 0 ? target.length + Number(key) : key;
        return Reflect.get(target, index)
    }
})

console.log(proxyArray[-1]); //5