Proxy API--Vue3響應式對象reactive初探

Proxy API對應的Proxy對象是ES2015就已引入的一個原生對象,用於定義基本操做的自定義行爲(如屬性查找、賦值、枚舉、函數調用等)。javascript

從字面意思來理解,Proxy對象是目標對象的一個代理器,任何對目標對象的操做(實例化,添加/刪除/修改屬性等等),都必須經過該代理器。所以咱們能夠把來自外界的全部操做進行攔截和過濾或者修改等操做。vue

基於Proxy的這些特性,經常使用於:java

  • 建立一個可「響應式」的對象,例如Vue3.0中的reactive方法。
  • 建立可隔離的JavaScript「沙箱」。

Proxy常見用法

Proxy語法:react

const p = new Proxy(target, handler)
複製代碼
  • target:要使用Proxy包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。
  • handler:以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理 p 的行爲。

例以下面一個很簡單的用法:git

let foo = {
	a: 1,
	b: 2
}
let handler = {
    get:(obj,key)=>{
        console.log('get')
        return key in obj ? obj[key] : undefined
    }
}
let p = new Proxy(foo,handler)
console.log(p.a) // 1
複製代碼

上面代碼中p就是foo的代理對象,對p對象的相關操做都會同步到foo對象上。github

同時Proxy也提供了另外一種生成代理對象的方法Proxy.revocable()api

const { proxy,revoke } = Proxy.revocable(target, handler)
複製代碼

該方法的返回值是一個對象,其結構爲: {"proxy": proxy, "revoke": revoke},其中:數組

  • proxy:表示新生成的代理對象自己,和用通常方式new Proxy(target, handler) 建立的代理對象沒什麼不一樣,只是它能夠被撤銷掉。
  • revoke:撤銷方法,調用的時候不須要加任何參數,就能夠撤銷掉和它一塊兒生成的那個代理對象。

例如:bash

let foo = {
	a: 1,
	b: 2
}
let handler = {
    get:(obj,key)=>{
        console.log('get')
        return key in obj ? obj[key] : undefined
    }
}
let { proxy,revoke } = Proxy.revocable(foo,handler)

console.log(proxy.a) // 1

revoke()

console.log(proxy.a) // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
複製代碼

須要注意的是,一旦某個代理對象被撤銷,它將變得幾乎徹底不可調用,在它身上執行任何的可代理操做都會拋出 TypeError 異常。app

Proxy的handler

上面代碼中,咱們只使用了get操做的handler,即當嘗試獲取對象的某個屬性時會進入這個方法,除此以外Proxy共有接近14個handler也能夠稱做爲鉤子,它們分別是:

handler.getPrototypeOf():
在讀取代理對象的原型時觸發該操做,好比在執行 Object.getPrototypeOf(proxy) 時。

handler.setPrototypeOf():
在設置代理對象的原型時觸發該操做,好比在執行 Object.setPrototypeOf(proxy, null) 時。

handler.isExtensible():
在判斷一個代理對象是不是可擴展時觸發該操做,好比在執行 Object.isExtensible(proxy) 時。

handler.preventExtensions():
在讓一個代理對象不可擴展時觸發該操做,好比在執行 Object.preventExtensions(proxy) 時。

handler.getOwnPropertyDescriptor():
在獲取代理對象某個屬性的屬性描述時觸發該操做,好比在執行 Object.getOwnPropertyDescriptor(proxy, "foo") 時。

handler.defineProperty():
在定義代理對象某個屬性時的屬性描述時觸發該操做,好比在執行 Object.defineProperty(proxy, "foo", {}) 時。

handler.has():
在判斷代理對象是否擁有某個屬性時觸發該操做,好比在執行 "foo" in proxy 時。

handler.get():
在讀取代理對象的某個屬性時觸發該操做,好比在執行 proxy.foo 時。

handler.set():
在給代理對象的某個屬性賦值時觸發該操做,好比在執行 proxy.foo = 1 時。

handler.deleteProperty():
在刪除代理對象的某個屬性時觸發該操做,即便用 delete 運算符,好比在執行 delete proxy.foo 時。

handler.ownKeys():
當執行Object.getOwnPropertyNames(proxy) 和Object.getOwnPropertySymbols(proxy)時觸發。

handler.apply():
當代理對象是一個function函數時,調用apply()方法時觸發,好比proxy.apply()。

handler.construct():
當代理對象是一個function函數時,經過new關鍵字實例化時觸發,好比new proxy()。
複製代碼

結合這些handler,咱們能夠實現一些針對對象的限制操做,例如:

  • 禁止刪除和修改對象的某個屬性
let foo = {
    a:1,
    b:2
}
let handler = {
    set:(obj,key,value,receiver)=>{
        console.log('set')
        if (key == 'a') throw new Error('can not change property:'+key)
        obj[key] = value
        return true
    },
    deleteProperty:(obj,key)=>{
        console.log('delete')
        if (key == 'a') throw new Error('can not delete property:'+key)
        delete obj[key]
        return true
    }
}

let p = new Proxy(foo,handler)

p.a = 3 // Uncaught Error

delete p.a  // Uncaught Error
複製代碼

其中,set方法的receiver一般是 Proxy 本即 p,可是當有一段代碼執行 obj.name = "jen", obj 不是一個 proxy,且自身不含 name 屬性,可是它的原型鏈上有一個 proxy,那麼,那個 proxy 的handler裏的set方法會被調用,而此時obj 會做爲 receiver 這個參數傳進來。

  • 對屬性的修改進行校驗
let foo = {
    a:1,
    b:2
}
let handler = {
    set:(obj,key,value)=>{
        console.log('set')
        if (typeof(value) !== 'number') throw new Error('can not change property:'+key)
        obj[key] = value
        return true
    }
}

let p = new Proxy(foo,handler)

p.a = 'hello' // Uncaught Error
複製代碼

Proxy和響應式對象reactive

Vue3中的響應式對象:

import {ref,reactive} from 'vue'
...
setup(){
  const name = ref('test')
  const state = reactive({
    list: []
  })
  return {
      name,
      state
  }
}
...
複製代碼

在Vue3中,composition-api提供了一種建立響應式對象的方法reactive,其內部就是利用了Proxy API來實現的,特別是藉助handler的set方法,能夠實現雙向數據綁定相關的邏輯,這對於Vue2.x中的Object.defineProperty()是很大的改變。

  • Object.defineProperty()只能單一的監聽已有屬性的修改或者變化,沒法檢測到對象屬性的新增或刪除,而Proxy則能夠輕鬆實現。

  • Object.defineProperty()沒法監聽屬性值是數組類型的變化,而Proxy則能夠輕鬆實現。

例如監聽數組的變化:

let arr = [1]
let handler = {
    set:(obj,key,value)=>{
        console.log('set')
        return Reflect.set(obj, key, value);
    }
}

let p = new Proxy(arr,handler)
p.push(2)
複製代碼

Reflect.set()用於修改數組的值,參考Reflect,可是對於多層對象嵌套問題,須要通過必定的處理:

let foo = {
    a:1,
    b:2
}
let handler = {
    set:(obj,key,value)=>{
        console.log('set')
        // 雙向綁定相關邏輯
        obj[key] = value
        return true
    }
}

let p = new Proxy(foo,handler)

p.a = 3
複製代碼

上面代碼中,對於簡單的對象foo是徹底沒問題的,可是若是foo是一個複雜對象,裏面嵌套的不少對象,那麼當去嘗試修改裏層對象的值時,set方法就不會觸發,爲了解決這種場景,在Vue3中,採用了遞歸的方式來解決這個問題:

let foo = {a:{c:3,d:{e:4}},b:2}
const isObject = (val)=>{
    return val !== null && typeof val === 'object'
}
const createProxy = (target)=>{
    let p = new Proxy(target,{
        get:(obj,key)=>{
            let res = obj[key] ? obj[key] : undefined

            // 判斷類型,避免死循環
            if (isObject(res)) {
                return createProxy(res)
            } else {
                return res
            }
        },
        set: (obj, key, value)=> {
          console.log('set')
          obj[key] = value;

        }
    })

    return p
}

let result = createProxy(foo)

result.a.d.e = 6 // 打印出set
複製代碼

當嘗試去修改一個多層嵌套的對象的屬性時,會觸發該屬性的上一級對象的get方法,利用這個就能夠對每一個層級的對象添加Proxy代理,這樣就實現了多層嵌套對象的屬性修改問題。

固然,上面這段代碼只是Vue3中reactive的一個縮影,更多的細節能夠瀏覽相關源碼來了解。

就目前來看,Porxy API相關內容是從ES2015才引入的標準,而且業界相關的polyfill也不是很完善,因此使用此API相關的框架要慎重的考慮兼容性問題。

相關文章
相關標籤/搜索