Vue之proxy和defineProperty

3,535 阅读3分钟

Vue3.0 中,响应式数据部分弃用了 Object.defineProperty,使用Proxy来代替它。

  1. Object.defineProperty 无法监听数组变化

Object.defineProperty 有一个缺陷是无法监听数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。

所以 Vue 才设置了 7 个变异数组(push、pop、shift、unshift、splice、sort、reverse)的 hack 方法来解决问题。

事实上,Object.defineProperty本身是可以监控到数组下标的变化的,只是在 Vue2.x 的实现中,从性能体验的性价比考虑,放弃了这个特性。

Object.defineProperty 在数组中的表现和在对象中的表现是一致的,数组的索引就可以看做是对象中的 key。

  • 通过索引访问或设置对应元素的值时,可以触发 getter 和 setter 方法。
  • 通过 push 或 unshift 会增加索引,对于新增加的属性,需要再手动初始化才能被 observe。
  • 通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 方法。
  1. Proxy

Proxy在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,可以对外界的访问进行过滤和改写。

ProxyObject.defineProperty的加强版。

  1. 对比
  • Object.defineProperty只能劫持对象的属性,对新增属性需要手动进行 Observe,而 Proxy 是直接代理对象。

由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,并返回一个新对象,不需要遍历操作。

// Object.defineProperty,对象obj的text属性进行劫持
const obj = {};
Object.defineProperty(obj, 'text', {
  get: function() {
    console.log('get val');
  },
  set: function(newVal) {
    console.log('set val:' + newVal);
    document.getElementById('input').value = newVal;
    document.getElementById('span').innerHTML = newVal;
  }
});

const input = document.getElementById('input');
input.addEventListener('keyup', function(e){
  obj.text = e.target.value;
})
// Proxy,劫持了整个obj对象
const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};

const newObj = new Proxy(obj, {
  get: function(target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key === 'text') {
      input.value = value;
      p.innerHTML = value;
    }
    return Reflect.set(target, key, value, receiver);
  },
});

input.addEventListener('keyup', function(e) {
  newObj.text = e.target.value;
});
  • Proxy可以直接监听数组的变化

当我们对数组进行操作(push、shift、splice等)时,会触发对应的方法名称和length的变化。

const list = document.getElementById('list');
const btn = document.getElementById('btn');

// 渲染列表
const Render = {
  // 初始化
  init: function(arr) {
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < arr.length; i++) {
      const li = document.createElement('li');
      li.textContent = arr[i];
      fragment.appendChild(li);
    }
    list.appendChild(fragment);
  },
  change: function(val) {
    const li = document.createElement('li');
    li.textContent = val;
    list.appendChild(li);
  },
};

// 初始数组
const arr = [1, 2, 3, 4];

// 监听数组
const newArr = new Proxy(arr, {
  get: function(target, key, receiver) {
    console.log(key);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key !== 'length') {
      Render.change(value);
    }
    return Reflect.set(target, key, value, receiver);
  },
});

// 初始化
window.onload = function() {
    Render.init(arr);
}

// push数字
btn.addEventListener('click', function() {
  newArr.push(6);
});
  • Proxy支持 13 种拦截操作

get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和proxy['foo']。

set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v 或 proxy['foo'] = v,返回一个布尔值。

has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。

deleteProperty(target, propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值。

ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。

getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。

preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。

getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。

isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。

setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。

construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

  • Proxy 作为新标准,从长远来看,JS 引擎会继续优化 ,但是兼容性差

参考:

mp.weixin.qq.com/s/O8iL4o8oP…

juejin.cn/post/684490…