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
本文为原创,未经授权,禁止任何媒体或个人自媒体转载
商业侵权必究,如需授权请联系[email protected]
标签: Javascript vue