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.