preface
Everyone has been asked how Vue two-way data binding works, right? It should also be quick to say that Object. DefineProperty makes every property of data a getter/setter, but this is only half the answer, because Object and Array are implemented differently, which is why the title is Object. (It is recommended to see the summary first, and then step by step to see the implementation process)
Basic knowledge of
Let’s start with the following concepts:
Declarative and imperative programming
This concept is popular point said, want to understand in detail can consult information
- Imperative: telling the computer how to do something, exactly what we tell it to do, no matter what the desired result is.
- Declarative: We just tell the computer what it wants and let it figure out how to do it. Here’s a simple example to compare the difference between the two,
// Given an array arr = [1, 2, 3], want a new array with each item incremented by one
const arr = [1.2.3];
// The imperative tells the browser to loop through the array, +1 for each element, and then push into the new array
let newArr1 = [];
for (let i = 0; i < arr.length; i++) {
newArr1.push(arr[i]+1);
}
console.log(newArr1) // Get the new array
// The declarative form tells the browser that each item in the new array is the same as each item in the old array
let result = arr.map(item= > {
return item + 1;
})
console.log(result) // A new array
Copy the code
Why do you say that? Because vue.js is declarative, writing Vue as required by the API documentation knows what to do. (Look back as if off topic, no matter when consolidate knowledge 😂)
Object.defineProperty
Define a new property on an object, or modify an existing property of an object, and return the object. Vue.js uses this method to modify the properties of a data object.
let name = 'test';
let obj = {};
Object.defineProperty(obj, 'name', {
configurable: true.// Can be modified, can be deleted
enumerable: true./ / can be enumerated
get: function() { // Read value trigger
console.log('Read data');
return name;
},
set: function(newVal) { // Assignment is triggered
if(name === newVal){
return;
}
console.log('reassign'); name = newVal; }})console.log(obj.name);
obj.name = 'assignment';
/ / print out
// Read data
// test
// reassign
/ / assignment
Copy the code
Here is already Vue Object two-way data binding principle.
What does Vue do to implement full Object two-way data binding?
Data Monitoring: “Use” and “change”
Object.defineproperty is used to monitor data. When getting a value, get will be triggered for corresponding operations. When setting data, set will be triggered to know whether the data has been changed. Is it clear that we can collect where the data is being used when the data is called to trigger the GET function? Then, at setup time, fire the set function to tell GET to do something about the collected dependencies? Ok, so let’s encapsulate Object.defineProperty for this understanding
defineReactive
function defineReactive(data, key, val) {
//let dep = [];
let dep = new Dep() / / modify
Object.defineProperty(data, key, {
enumerable: true.configurable: true.get: function() {
// Collect dependencies
// dep.push(window.target) // window.target will define 6 operations
dep.depend() / / modify
return val
},
set: function(newVal){
if(val === newVal){
return
}
// Trigger dependencies
// for(let i=0; i<dep.length; i++){
// dep[i](newVal, val);
// }
dep.notify() / / modify
val = newVal
}
})
}
Copy the code
The dependency collection is stored in the DEP array on get, and every dependency in the DEP is triggered when set is triggered. In the source code is the DEP package into a class, to manage dependencies, the following implementation of the DEP class it.
Dep class
export default class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
depend() {
if(window.target){
this.addSub(window.target) // what is window.target?
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // window.target update method}}}function remove (arr, item) {
if(arr.length){
const index = arr.indexOf(item)
if(index > - 1) {return arr.splice(index, 1)}}}Copy the code
The Dep class we encapsulated can collect dependencies, delete dependencies, and notify dependencies, so we need to use the Dep class and modify defineReactive above. The dependencies collected by the Dep are known by code as window.target. When data changes, the window.target update method is called in response to the update.
Watcher class
There is a Watcher class in the source code, and its instance is the window.target we collected. Let’s take a look at one of the uses in Vue
vm.$watch('user.name'.function(newVal, oldVal){
console.log('My new name' + newVal); // The update function
})
Copy the code
When data.user.name is modified in a Vue instance, function will be executed, which means that this function needs to be added to the dependency of data.user.name. The data.user.name get method can be adjusted. So all Watcher has to do is add its own instance to the Dep of the corresponding property, and also have the ability to notify to update, so Watcher
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = this.get() // Get the initial value
}
get() {
window.target = this // By exposing the current instance to the Dep, the Dep knows who the dependency is
let value = this.getter.call(this.vm, this.vm) // Trigger the get method on the corresponding attribute on the VM instance to collect the dependency
window.target = undefined // Give it to someone else
return value
}
update() {
const oldValue = this.value / / the old value
this.value = this.get() // Get a new value
this.cb.call(this.vm, this.value, oldValue)
}
}
Copy the code
If you look back at some of the programs I’ve written above, you’ll see that there are clever combinations, especially with the Watcher example, which adds itself to the Dep, which I think is pretty cool anyway. It also shows that de$watch in Vue is implemented via Watcher.
Watcher knows that parsePath returns a method that returns a value when it is called
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.spilt('. ')
return function(obj){
for(let i = 0; i < segments.length; i++){
if(! obj)return
obj = obj[segments[i]]
}
return obj
}
}
Copy the code
This. Getter. Call (this.vm, this.vm) in Watcher refers parsePath’s return function to this.vm and passes this as an argument.
The Observer class
Every attribute of data in Vue is detected. In fact, we use defineReactive to detect if a data has a lot of attributes that need to be called many times. Observer is a utility class that turns each attribute into a getter/setter for the code
export class Observer {
constructor(value) {
this.value = value
if(!Array.isArray(value)){
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key= > {
defineReactive(obj, key, obj[key])
})
}
}
Copy the code
The new Observer(obj) can change all properties of obj into getters/setters. What if obj[key] is still an object? Define obj[key]; define obj[key]; define obj[key]
function defineReactive(data, key, val) {
if(typeof val === 'object') {
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true.configurable: true.get: function() {
dep.depend()
return val
},
set: function(newVal){
if(val === newVal){
return
}
dep.notify()
val = newVal
}
})
}
Copy the code
Vue provides $set and $delete to implement these two functions. Vue provides $set and $delete to implement these two functions. The implementation of these two is not difficult.
conclusion
For Object’s data response in Vue, MY summary is “define getters/setters for standby,” use “: collect dependencies,” change “: trigger dependencies
- Standby: Pass
Observer
anddefineReactive
Change the property togetter/setter
; - Use: pass
Watcher
ingetter
To collect dependencies fromdep
; - “Change” : pass
setter
telldep
The data has changed,dep
noticeWatcher
To update;