乐趣区

关于前端:初识Proxy

最近在温习 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 写入属性
has in 操作符
deleteProperty delete 操作符
apply 函数调用
construct new 操作符
getPrototypeOf Object.getPrototypeOf
setPrototypeOf Object.setPrototypeOf
isExtensible Object.isExtensible
preventExtensions Object.preventExtensions
defineProperty Object.defineProperty,
Object.defineProperties
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor,
for…in,
Object.keys/values/entries
ownKeys Object.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 的。

怎么通过 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
退出移动版