In fact, many articles on this question have been written, and it is also a high-frequency topic for interviews. This is just to record my own understanding.

The difference between Proxy and Object.defineproperty

  1. Object.defineProperty can only hijack the properties of the object, and deep traversal is required for nested objects; while Proxy directly proxy the entire object
  2. Object.defineProperty requires manual Observe (using $set) for the new attributes; Proxy can intercept the new attributes of the object, and the array of push , shift , splice can also be intercepted
  3. Proxy has 13 interception operations, which defineProperty does not have
  4. Proxy poor compatibility IE browser does not support very many Proxy way there is no complete polyfill program

defineProperty writing method;

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
     get: function defineGet() {
      console.log(`get key: ${key} value: ${value}`)
      return value
    },
     set: function defineSet(newVal) {
      console.log(`set key: ${key} value: ${newVal}`)
      value = newVal
    }
  })
}
function observe(data) {
  Object.keys(data).forEach(function(key) {
    // 递归的getter setter
    defineReactive(data, key, data[key])
  })
}

The wording of Proxy

let proxyObj = new Proxy(data, {
    get(key) {
        return data[key]
    },
    set(key, value) {
        data[key] = value
    }
})

Of course there are other attributes, the simplest is written here.

The difference between these two methods reminds me of event proxy

<ul id="ul">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>

If no event agency, then it will give ul each under li binding events, wrote that there is a problem, new li are no events , no events added to it together.
If you are using an event proxy, then the newly added child node will also have an event response, because it triggers the event by triggering the proxy node (parent node bubbling)
Very similar, what I want to explain here is: defineProperty getter/setter on its own object properties, and Proxy returns a proxy object. Only when the proxy object is modified will the response type occur. If the original object property is modified, it does not Will generate responsive updates.

Object.defineProperty processing of arrays

Check vue of official document we can see:

Vue cannot detect changes in the following arrays:

1. When you use the index to directly set an array item, for example: vm.items[indexOfItem] = newValue
2. When you modify the length of the array, for example: vm.items.length = newLength

For the first point:
Some articles are written directly

Object.defineProperty has a defect that it is unable to monitor the changes of the array, which causes the array to set the value directly through the subscript of the array, and it cannot respond in real time.

This statement is error . In fact, Object.defineProperty can monitor the change of the array index, but in Vue , this feature is abandoned from the consideration of performance/experience.
For the index under the array, getter/setter can be used,

image.png

But why didn't vue do this? If you monitor the index value, push or unshift not been hijacked, nor is it responsive, you need to manually perform observe , pop or shift , it will delete and update the index, and it can also trigger the response. , But the array is often traversed, which triggers many index getter performance is not very good.

For the second point:
MDN:

Redefining the length property of an array is possible, but it is subject to general redefinition restrictions. (The length attribute is initially non-configurable, non-enumerable, and writable. For an array with constant content, it is possible to change the value of its length attribute or make it non-writable. But change its enumerability and Configurability or trying to change its value or writeability when it is non-writable, both of which are not allowed.) However, not all browsers allow the redefinition of Array.length.

image.png

image.png

Therefore, for the array of length , it is impossible to perform get and set accessor attributes, so it cannot be updated responsively.

Note here that there are two concepts: index and subscript
The array has a subscript, but the corresponding subscript may not have an index value!

arr = [1,2]
arr.length = 5
arr[4] // empty 下标为4,值为empty,索引值不存在。 for..in 不会遍历出索引值不存在的元素

Manually assign length to a larger value. At this time, the length will be updated, but the corresponding index will not be assigned, that is, the attribute of the object is not. defineProperty cannot handle the monitoring of unknown attributes. For example: the array of length = 5 There is an index of 4. If this index (attribute) does not exist, setter is impossible.

array is actually the same as the key performance of the object.

vue the array separately, hijacked it and rewritten it,
Look at an array hijacked demo :

const arrayProto = Array.prototype

// 以arrayProto为原型的空对象
const arrayMethods = Object.create(arrayProto)

const methodToPatch = ['push', 'splice']

methodToPatch.forEach(function (method) {
    const original = arrayProto[method]
    
    def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args)
        console.log('劫持hh')
        return result
    })
})

function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        configurable: true,
        writable: true
    })
}

let arr = [1,2,3]
arr.__proto__ = arrayMethods

arr.push(4)
// 输出
// 劫持hh
// 4

arrayMethods using the array as the prototype, and defined the array to be hijacked on it, we just simply printed a sentence. Change arr prototype points (to __proto__ assignment), in arr operation push,splice method will take the hijacking of time. The array hijacking of vue actually adds the logic responsive

function mutator(...args) {
    // cache original method
  const original = arrayProto[method]
  // obj key, val, enumerable
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        //eg: push(a) inserted = [a] // 为push的值添加Oberserve响应监听
        inserted = args
        break
      case 'splice':
        // eg: splice(start,deleteCount,...items)  inserted = [items] //  为新添加的值添加Oberserve响应监听
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
}
/**
 * Observe a list of Array items.
 */
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

$set manually add responsive principle

For the new attribute of the object/new element of the array, the response type cannot be triggered, we can use vue $set for processing

vm.$set(obj,key,value)

For arrays, the splice method can also be used:

vm.items.splice(indexOfItem, 1, newValue)

But they are essentially the same!

The core of the implementation of set is:

  1. If it is an array, use splice the elements manually observe
  2. If it is an object
    If it is to modify the existing key, direct assignment will trigger a responsive update
    If it is a new key, manually observe
  3. If it is not a reactive object (the reactive object has __ob__ attribute), just assign it directly

The internal implementation of set:

export function set (target: Array<any> | Object, key: any, val: any): any {
  // 如果 set 函数的第一个参数是 undefined 或 null 或者是原始类型值,那么在非生产环境下会打印警告信息
  // 这个api本来就是给对象与数组使用的
  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)) {
    // 类似$vm.set(vm.$data.arr, 0, 3)
    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
    target.length = Math.max(target.length, key)
    // 利用数组的splice变异方法触发响应式, 这个前面讲过
    target.splice(key, 1, val)
    return val
  }
  // target为对象, key在target或者target.prototype上。
  // 同时必须不能在 Object.prototype 上
  // 直接修改即可, 有兴趣可以看issue: https://github.com/vuejs/vue/issues/6845
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__
  // Vue 实例对象拥有 _isVue 属性, 即不允许给Vue 实例对象添加属性
  // 也不允许Vue.set/$set 函数为根数据对象(vm.$data)添加属性
  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
  }
  // target本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  // ---->进行响应式处理
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

refer to:
https://www.zhihu.com/question/51520173
https://www.javascriptc.com/3058.html
https://juejin.cn/post/6844903614096343047


高压郭
961 声望494 粉丝

从简单到难 一步一步