最近在复习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

kerin
497 声望573 粉丝

前端菜鸟