Object.defineProperty 定义

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

Object.defineProperty 拦截

在 es5 通过 Object.defineProperty() 可以拦截,vue2 就是通过这个方法来拦截属性的 get,set 方法

const obj = { a: 1 };
let value = null;
Object.defineProperty(obj, 'a', {
get: function () {
const val = value || 100;
console.log(`get object['a'] value ${val}`);
return val;
},
set(val) {
console.log(`set object['a'] value ${val}`);
value = val;
},
});
obj.a; // get object['a'] value 100
obj.a = 10; // set object['a'] value 10

上面一个方法一个缺点就是没法监听到数组的变化,在 vue 内部做了一层数组方法拦截,源码是这样实现的,通过Object.defineProperty(arr, 'push', { ... })改写对应的数组方法

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method) {
  // 获取原始的数组操作方法
  var original = arrayProto[method];
  // 在 arrayMethods 上新建属性 method,并为 method 指定值(函数)
  // 即改写 arrayMethods 上的同名数组方法
  def(arrayMethods, method, function mutator() {
    var arguments$1 = arguments;
    var i = arguments.length;
    var args = new Array(i);
    while(i--) {
      args[i] = arguments$1[i];
    }
    var result = original.apply(this, args);
    // 因 arrayMethods 是为了作为 Observer 中的 value 的原型或者直接作为属性,所以此处的 this 一般就是指向 Observer 中的 value
    // 当然,还需要修改 Observer,使得其中的 value 有一个指向 Observer 自身的属性,__ob__,以此将两者关联起来
    var ob = this.__ob__;
    // 存放新增的数组元素
    var inserted;
    // 对几个可能有新增元素的方法单独考虑
    switch(method) {
      case 'push':
        inserted = args;
        break;
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        // splice 方法第三个参数开始才是新增的元素
        inserted =args.slice(2);
        break;
    }
    if(inserted) {
      // 对新增元素进行 getter、setter 绑定
      ob.observerArray(inserted);
    }
    // 触发方法
    ob.dep.notify();
    return result;
  })
}

如果是新增对象属性或者修改数组某个值(arr[0]),vue 还是无法拦截到,我们一般通过 Vue.set 或者 this.$set 来实现,set 的方法实现如下

function set(target: Array<any> | Object, key: any, val: any): any {
if (
process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
);
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' &&
warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val;
}
if (!ob) {
target[key] = val;
return val;
}
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}

Proxy 的定义

Proxy 可以在目标对象之前设置拦截,外界访问该对象必须通过这层拦截,根据阮老师的文档可以找出,我们可以拦截对象的操作有 13 中,分别是

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['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 拦截

通过 Proxy 对象也可以监听到属性的变化,一个监听对象 get,set 的实现

const obj = {};
const objProxy = new Proxy(obj, {
get: function (target, prop) {
console.log(`get object['${prop}'] value ${target[prop]}`);
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`set object['${prop}'] value ${value}`);
target[prop] = value;
},
});
objProxy.a;
objProxy.a = 10;

当然也很简单监听数组的变化或者对应下标值的变化

const arr = [1, 2, 3];
const arrProxy = new Proxy(arr, {
get(target, prop) {
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`set prop = ${prop} value = ${value}`);
Reflect.set(target, prop, value);
return true;
},
});
arrProxy[0] = 4;