Vue新老数据双向绑定比较

917 阅读3分钟

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

get

属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined

set

属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined

因此,可以通过设置属性的 set 方法来实现监听数据变化的效果。

const obj = {
    a: 1,
    b: [1, 2],
};
for (let key in obj) {
  let _val = obj[key];
  Object.defineProperty(obj, key, {
    get() {
      console.log('get ', key);
      return _val;
    },
    set: (value) => {
      console.log(`set ${key}: `, value);
      _val = value;
    },
  });
}
obj.a = 2;
console.log(obj.a);
obj.b = [3, 4];
console.log(obj.b);

// set a 2
// get a
// 2
// set b [3, 4]
// get b
// [3, 4]

这样一看没什么问题, 但是当通过数组方法去改变值时,会发现并没有被 set 监听到。

obj.b.push(5);
console.log(obj.b);

// [1, 2, 5] b的值正确没问题,push进去了,但是没有set b

因为我们是调用了数组方法,而不是通过 obj.b = [] 重新赋值,所以解法也简单:重写数组方法,让我们的数组去继承这些方法

const obj = {
    a: 1,
    b: [1, 2],
};
const aim = []; // 定义一个工具人数组,下面重写其方法,最后让其他数组的去继承其方法
const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];  
// 重写工具人的方法
arrayMethods.forEach(item => {
    Object.defineProperty(aim, item, {
        value() {
            console.log(`call ${item}`);
            [][item].apply(this, arguments);
        },
    })
});
obj.b.__proto__ = aim; // 让我们的数据继承工具人的方法
obj.b.push(3);
console.log(obj.b);

// call push
// [1, 2, 3]

简单整合后如下

const obj = {
    a: 1,
    b: [1, 2],
};
const aim = []; // 定义一个数组,下面重写其方法,最后让其他数组的去继承其方法
const arrayMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
];

arrayMethods.forEach(item => {
  Object.defineProperty(aim, item, {
    value() {
      console.log(`call ${item}`);
      [][item].apply(this, arguments);
    },
  })
});

for (let key in obj) {
  if (Array.isArray(obj[key])) {
    obj[key].__proto__ = aim;
  } else {
    let _val = obj[key];
    Object.defineProperty(obj, key, {
      get() {
        console.log('get ', key);
        return _val;
      },
      set: (value) => {
        console.log(`set ${key}: `, value);
        _val = value;
      },
    });
  }
}

obj.a = 2;
console.log(obj.a);
obj.b.push(3);
console.log(obj.b);

Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

语法

const p = new Proxy(target, handler)

target

要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler

一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

原理和上面类似,监听属性值变化

const obj = {
    a: 1,
    b: [1, 2],
};
const objProxy = new Proxy(obj, {
  get(target, prop, receiver) {
    console.log('get', prop);
    return target[prop];
  },
  set(target, prop, value, receiver) {
    console.log('property set: ' + prop + ' = ' + value);
    target[prop] = value;
    return true;
  },
});
objProxy.a = 2;
console.log(objProxy.a);

// property set: a = 2
// get a
// 2

objProxy.b.push(3);
console.log(objProxy.b);

// get b
// get b
// 3

通过上面代码可以看到,属性 a 的监听没问题,但是属性 b 数组的变化还是没有监听到,因为通过这种方式我们监听的是 b 地址值(数组是由一个地址值地址值指向的堆空间里的数据组成的),我们通过 push 并没有改变地址值,而是改变堆栈中的值,所以没有监听到。解决方法也简单:如果是数组,再次创建一个 Proxy 对象去监听

const obj = {
    a: 1,
    b: [1, 2],
};
const fnProxy = (obj) => {
  const objProxy = new Proxy(obj, {
    get(target, prop, receiver) {
      console.log('get', prop);
      if (Array.isArray(target[prop])) {
        return fnProxy(target[prop]);
      }
      return target[prop];
    },
    set(target, prop, value, receiver) {
      console.log('property set: ' + prop + ' = ' + value);
      target[prop] = value;
      return true;
    },
  });
  return objProxy;
};
fnProxy(obj);
const objProxy = fnProxy(obj);
objProxy.b.push(10);
console.log(objProxy.b);

// get b
// get push
// get length
// property set: 2 = 10   这个 ‘2’ 是数组下标
// property set: length = 3
// get b
// [1, 2, 10]

上面的代码中将生成Proxy对象的操作封装了一个方法,如果目标值是数组,就将目标值作为入参再次调用该方法,通过递归的方式完成对数组变化的监听。