One, foreword

As one of the core functions of Vue, two-way data binding is mainly implemented in two parts:

  • The data was hijacked
  • Publish and subscribe model

This article will focus on Vue’s approach to data hijacking, and the next will cover the design of the publish/subscribe model.

Second, for Object type hijacking

For the Object type, the read and set operations of its properties are hijacked. In JavaScript, an object’s properties consist mainly of a string “name” and a “property descriptor”, which includes the following options:

  • Value: The value of this attribute;
  • Writable: This property can be changed only when the value is true.
  • Get: getter;
  • Set: setter;
  • The property can be deleted and the property descriptor can be changed only if the value is true.
  • Enumerable: This property can be enumerable only if it is true.

The above setters and getters are for developers to read and set properties, and the object.defineProperty () method is used to set Object property descriptors:

function defineReactive (obj, key) {
  let val = obj[key]
  Object.defineProperty(obj, key, {
    get () {
      console.log('collect dependencies ===')
      console.log('Current value:' + val)
      return val
    },
    set (newValue) {
      console.log('notification of change ===')
      console.log('Current value:' + newValue)
      val = newValue
    }
  })
}

const student = {
  name: 'xiaoming'
}

defineReactive(student, 'name') // Hijack the read and set operations for the name attribute

Copy the code

The above code hijacks the read and set operations for the name property in the Student Object by setting the setter and getter methods for the property through the object.defineProperty () method.

As you can see, this method can only set one property at a time, so you need to traverse the object to complete the configuration of its properties:

  Object.keys(student).forEach(key= > defineReactive(student, key))
Copy the code

It also has to be a specific attribute, which can be deadly.

If you later need to extend the object, you must manually set setter and getter methods for the new property. ** This is why properties that are not declared in data do not automatically have two-way binding effects **. (Call vue.set () to set it manually.)

This is the core implementation of object hijacking, but there are some important details to note:

1. Attribute descriptor – 64x

In JavaScript, when an object is created from a literal, its property descriptor defaults to the following:

const foo = {
  name: '123'
}
Object.getOwnPropertyDescriptor(foo, 'name') // { value: '123', writable: true, enumerable: true, configurable: true }
Copy the code

If the setter and getter is set to freely control any additional information, no additional control system can be configured, exercising any additional control, and exercising any additional control system. Otherwise an exception will be thrown when the object.defineProperty () method is used:

// Some of the duplicate code is not listed here.
function defineReactive (obj, key) {
  // ...

  const desc = Object.getOwnPropertyDescriptor(obj, key)

  if (desc && desc.configurable === false) {
    return
  }

  // ...
}
Copy the code

In JavaScript, there are many different cases that cause the 64x value to be false:

  • The property can already be configured using the Object.defineProperty() method.
  • The object.seal () method is used to set the Object as a sealed Object. Only the value of the attribute can be modified and the attribute cannot be deleted or the descriptor of the attribute can be modified.
  • Freeze the Object with the object.freeze () method, which is stricter than the object.seal () method in that it does not allow the value of the property to be changed.
Default getter and setter methods

In addition, the developer may have already set getter and setter methods for the properties of the object. In this case, Vue cannot break the developer-defined methods, so it also protects the default getter and setter methods:

// Some of the duplicate code is not listed here
function defineReactive (obj, key) {
  let val = obj[key]

  //....

  // Default getter setter
  const getter = desc && desc.get
  const setter = desc && desc.set

  Object.defineProperty(obj, key, {
    get () {
      const value = getter ? getter.call(obj) : val // Take precedence over the default getter
      return value
    },
    set (newValue) {
      const value = getter ? getter.call(obj) : val
      // There is no need to update the pit NaN!!!! of === if the values are the same
      if(newValue === value || (value ! == value && newValue ! == newValue)) {return
      }

      if(getter && ! setter) {// The user did not set the setter
        return
      }

      if (setter) {
        // Call the default setter method
        setter.call(obj, newValue)
      } else {
        val = newValue
      }
    }
  })
}
Copy the code
3. Recursive attribute values

Last but not least, if the value of an attribute is also an object, then the value of an object’s attribute needs to be processed recursively:

function defineReactive (obj, key) {
  let val = obj[key]

  // ...

  // Process its attribute values recursively
  const childObj = observe(val)

  // ...
}
Copy the code

Recursive loop reference objects are prone to recursive stack burst, in which case Vue avoids recursive stack burst by defining OB objects that record objects that have getter and setter methods set.

function isObject (val) {
  const type = val
  returnval ! = =null && (type === 'object' || type === 'function')}function observe (value) {
  if(! isObject(value)) {return
  }

  let ob
  // Avoid recursive stack explosion caused by circular references
  if (value.hasOwnProperty('__ob__') && value.__obj__ instanceof Observer) {
    ob = value.__ob__
  } else if (Object.isExtensible(value)) {
    // You need to define attributes such as __ob__, so you need to be able to extend them
    ob = new Observer(value)
  }

  return ob
}
Copy the code

Object extensibility is mentioned in the above code. In JavaScript, all objects are extensible by default, but methods are provided to allow objects to be unextensible:

const obj = { name: 'xiaoming' }
Object.preventExtensions(obj)
obj.age = 20
console.log(obj.age) // undefined
Copy the code

In addition to the above methods, there are the object.seal () and object.freeze () methods mentioned earlier.

Iii. Array hijacking

An array is a special kind of Object whose subscripts are actually properties of the Object, so you could theoretically handle array objects with object.defineProperty ().

However, Vue does not use the above method to hijack array objects, mainly due to the following two factors :(readers have better insights, please leave a comment.)

1. The special length attribute

The descriptor for the length property of an array object is inherently unique:

const arr = [1.2.3]

Object.getOwnPropertyDescriptor(arr, 'length').configurable // false
Copy the code

This means that you cannot hijack the reading and setting methods of the Length attribute through the Object.defineProperty() method.

Compared with the properties of objects, the array subscript changes relatively frequently, and the method of changing the array length is more flexible, once the array length changes, so in the case of not automatically aware, the developer can only manually update the new array subscript, which is a very tedious work.

2. Array operation scenarios

Arrays are mostly traversal, and attaching a GET and set method to each element is a performance burden.

3. Hijacking array methods

In the end, Vue chose to hijack some common array manipulation methods to know what was happening to the array:

const methods = [
  'push'.'pop'.'shift'.'unshift'.'sort'.'reverse'.'splice'
]
Copy the code

The hijacking of Array methods involves a lot of prototype-related knowledge. First, Array instance methods are mostly derived from array.prototype objects.

However, you can’t tamper with the array. prototype object directly, which affects all Array instances. To avoid this, you need to use prototype inheritance to get a new prototype object:

const arrayProto = Array.prototype
const injackingPrototype = Object.create(arrayProto)
Copy the code

With the new prototype object in hand, rewrite these common operations:

methods.forEach(method= > {
  const originArrayMethod = arrayProto[method]
  injackingPrototype[method] = function (. args) {
    const result = originArrayMethod.apply(this, args)
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) {
      // For new elements, continue hijacking
      // ob.observeArray(inserted)
    }
    // Notification changes
    return result
  }
})
Copy the code

Finally, update the prototype of the jacked array instance. Prior to ES6, you could specify the prototype via the browser private property Proto. After that, you could use the following method:

Object.setPrototypeOf(arr, injackingPrototype)
Copy the code

By the way, when the vue.set () method is used to set array elements, the Vue is actually calling the hijacked splice() method internally to trigger the update.

Four,

According to the above, data hijacking in Vue is divided into two parts:

  • For Object types, the object.defineProperty () method hijacks the reading and setting methods of attributes;
  • For Array types, knowledge of stereotypes is used to hijack common functions to know that the current Array has changed.

And the Object.defineProperty() method has the following defects:

  • Only one specific attribute can be set at a time, resulting in the need to traverse the object to set the attribute, and also resulting in the inability to detect the new attribute;
  • The property descriptor, signals, and signals are different and fatal.

Proxy in ES6 can solve these problems perfectly (compatibility is a big problem at present), which is also a big move in Vue3.0. Interested readers can check relevant information.

If this article has been helpful to you, take a note and give me some encouragement.