1 What is the Vue response
❝
When the data changes, the page is re-rendered, which is called Vue responsiveness
❞
2 What do we need to do to complete this process
- Detect changes in data
- Collect what data the view depends on
- When data changes, automatically “notify” the part of the view that needs to be updated and update it
“They correspond to professional idioms:“
- Data hijacking/data proxy
- Depend on the collection
- Publish and subscribe model
3 How to detect changes in data
There are two ways to detect changes: using Object.defineProperty and ES6 proxies, which is data hijacking or data proxying.
3.1 Object. DefineProperty implementation
Vue listens for changes in data by setting setter/getter methods for object properties, relies on the getter for collection, and each setter method is an observer that notifies the subscriber to update the view when the data changes.
The code is as follows:
function render () {
//setI'm going to go here and re-render console.log('Simulated View Rendering')
}
let data = {
name: 'Boat on the waves'. location: { x: 100, y: 100 } } observe(data) Copy the code
Defining the core function
functionObserve (obj) {// We use it to make the object observable// Determine the type if(! obj || typeof obj ! = ='object') {
return
}
Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) function defineReactive (obj, key, value) { // Recursive subattributes observe(value) Object.defineProperty(obj, key, { enumerable: true, // enumerable (traversable) configurable: true, // configurable (for example, can be deleted) get: function reactiveGetter () { console.log('get', value) // listen return value }, set: function reactiveSetter (newVal) { Observe (newVal) // If the assignment is an object, recurse the child property as well if(newVal ! == value) { console.log('set', newVal) // listen render() value = newVal } } }) } } Copy the code
Changing the property of data sets set; Then get the property of data, which triggers get
data.location = {
x: 1000,
y: 1000
} / / printset{x: 1000,y: 1000} Simulate view renderingData. name // Print getCopy the code
“The main function of this code is: “Observe this function detects object changes by passing in an obj (the object that needs to be tracked) and passing through defineReactive for each attribute, adding set and GET methods to each attribute. It’s worth noting that Observe makes recursive calls.
“How to detect data in Vue data is also very simple:“
class Vue {
/* Vue constructor class */ constructor(options) {
this._data = options.data;
observer(this._data);
} } Copy the code
So as soon as we create a new Vue object, we will track the changes in data.
“But we found a problem that the above code could not detect the addition or removal of object attributes (such as data.location.a=1, adding an A attribute).” This is because Vue uses object.defineProperty to convert an Object’s key into a getter/setter to track changes, but getter/setter can only track whether a data has been modified, not new or deleted attributes. If it is a delete attribute, we can implement it with vm.$delete, but what if it is a new attribute?
- You can use the vue.set (location, a, 1) method to add responsive properties to nested objects;
- You can also reassign this object, such as data.location = {… data.location,a:1}
Object. DefineProperty can’t listen for array changes and needs to override array methods
3.2 the Proxy implementation
Proxy is a new feature in JavaScript 2015. Proxy’s Proxy is for the entire Object, not an attribute of the Object, so unlike Object.defineProperty, which must traverse every attribute of the Object, Proxy only needs to do one layer of Proxy to listen for all attribute changes in the hierarchical structure. Of course, for deep structures, recursion is required. In addition, Proxy supports Proxy array changes.
function render() {
console.log('Mock View Update')
}
let obj = {
name: 'Front End Craftsman'. age: { age: 100 }, arr: [1, 2, 3] } let handler = { get(target, key) { // If the value is an object, then the object is data hijacked if (typeof target[key] == 'object'&& target[key] ! == null) { return new Proxy(target[key], handler) } return Reflect.get(target, key) }, set(target, key, value) { // If the key is length, the last attribute is traversed if (key === 'length') return true render() return Reflect.set(target, key, value) } } let proxy = new Proxy(obj, handler) proxy.age.name = 'Boat on the waves'// New attributes are supportedConsole. log(proxy.age.name) // Update the simulation viewproxy.arr[0] = 'Boat on the waves'// Support changes in the contents of arraysConsole. log(proxy.arr) // Update to mock view ['Boat on the waves', 2, 3]Proxy. Arr. Length - / / is invalidCopy the code
The above code is not only concise, but also implements a set of code for both object and array detection. But Proxy compatibility is not good!
4. Collect dependencies
4.1 Why Collect dependencies
❝
The purpose of looking at data is to notify where it has been used when its properties change. For example, the location data is used in the template, and when it changes, notifications are sent to where it is used.
❞
let globalData = {
text: 'Boat on the waves'
};
let test1 = new Vue({
template:
`<div> <span>{{text}}</span> <div>`, data: globalData }); let test2 = new Vue({ template: `<div> <span>{{text}}</span> <div>`, data: globalData }); Copy the code
If we execute this statement:
globalData.text = 'Front End Craftsman';
Copy the code
At this point, we need to notify test1 and test2, the two Vue instances, to update the view. Only by collecting dependencies can we know where to rely on my data and distribute updates when the data is updated. So how does dependency collection work? The core idea is the “event publish and subscribe model”. Let’s start by introducing two important actors — subscriber Dep and Watcher — and then explain how the collection dependencies are implemented.
4.2 Subscriber Dep
Collecting dependencies requires finding a place to store dependencies for dependencies, so we create a Dep that collects dependencies, removes dependencies, sends messages to dependencies, and so on. So let’s start by implementing a subscriber Dep class that decouples the properties of dependency collection and dispatch updates, “to be more specific” : its main purpose is to hold Watcher observer objects. We can think of Watcher as an intermediary, notifying it when data changes, and then notifying it elsewhere.
“A simple implementation of Dep:“
class Dep {
constructor () {
/* An array of Watcher objects */ this.subs = [];
}
/* Add a Watcher object to subs */ addSub (sub) { this.subs.push(sub); } /* Notify all Watcher objects to update the view */ notify () { this.subs.forEach((sub) => { sub.update(); }) } } Copy the code
The above code does two things:
- The addSub method can be used in the current
Dep
ObjectWatcher
Subscribe operation; - Notify the present with notify
Dep
The object’ssubs
All of theWatcher
Object triggers the update operation. So when the need arisesDepend on the collection
Call addSub when neededDistributed update
Is called notify.
The call is also simple:
let dp = new Dep()
Dp.addsub (() => {// When collecting dependencies console.log('emit here')
})
Dp.notify ()// When updates are sentCopy the code
5 Watcher
5.1 Why was Watcher introduced
A Watcher class is defined in Vue to represent the observation subscription dependency. As for why Watcher was introduced, Vue.js offers a good explanation:
When the attribute changes, we need to inform the place where the data is used, and there are many places where the data is used, and the type is different. It may be a template or a watch written by the user. At this time, we need to abstract out a class that can deal with these cases in a centralized manner. We then collect only instances of the encapsulated class in the dependency collection phase, notify it only, and let it take care of notifying the rest of the class.
“The purpose of dependency collection is to:” Store the observer Watcher object in the subs of the subscriber Dep in the current closure. The resulting relationship is shown below (see “Anatomy of vue.js internals” for a diagram).
5.2 Simple implementation of Watcher
class Watcher {
constructor(obj, key, cb) {
// Point dep. target to yourself// Then trigger the getter for the property to add a listener// Dep. Target is left blank Dep.target = this this.cb = cb this.obj = obj this.key = key this.value = obj[key] Dep.target = null } update() { // Get a new value this.value = this.obj[this.key] // We define a cb function that simulates view updates this.cb(this.value) } } Copy the code
The above is a simple implementation of Watcher. When the constructor is executed, the dep. target is pointed at itself, so that the corresponding Watcher is collected, and the corresponding Watcher is retrieved when the update is issued, and then the update function is executed.
“The nature of dependence:” The so-called dependence, in fact, is Watcher. How to collect dependencies can be summed up in one sentence: Collect dependencies in getters and fire dependencies in setters. Collect the dependencies first, where the data is used, and then trigger the previously collected dependency loop when the property changes.
Specifically, when the outside world reads data through the Watcher, the getter is fired to add the Watcher to the dependency, and whichever Watcher fires the getter is collected into the Dep. When the data changes, we loop through the dependency list, notifying all the Watchers.
Finally, we modified defineReactive function and added codes related to collect and distribute updates to the custom function to achieve a simple data response:
function observe (obj) {
// Determine the type if(! obj || typeof obj ! = ='object') {
return
}
Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) function defineReactive (obj, key, value) { Observe (value) // Recursive subattribute letDp = new Dep() // New Object.defineProperty(obj, key, { enumerable: true, // enumerable (traversable) configurable: true, // configurable (for example, can be deleted) get: function reactiveGetter () { console.log('get', value) // listen// Add Watcher to the subscription if (Dep.target) { Dp. AddSub (Dep., target) / / new } return value }, set: function reactiveSetter (newVal) { Observe (newVal) // If the assignment is an object, recurse the child property as well if(newVal ! == value) { console.log('set', newVal) // listen render() value = newVal // Execute watcher's update method/ / new dp. Notify () } } }) } } class Vue { constructor(options) { this._data = options.data; observer(this._data); /* Create a new Watcher observer object. Dep.target will point to the Watcher object */ new Watcher(); console.log('Simulated View Rendering'); } } Copy the code
When the Render function is rendered, the reactiveGetter function is triggered to collect the current Watcher object (stored in dep.target) into the Dep class by reading the desired object value. If you then change the value of the object, the reactiveSetter method is triggered, notifying the Dep class to call notify to trigger the update method on all Watcher objects to update the corresponding view.
“Complete flow chart:“
- After new Vue(), Vue calls the _init function for initialization, which is the init process, in which Data is converted to getter/setter form via Observer to keep track of changes to the Data. Getter functions are executed when the set object is read, and setter functions are executed when the value is assigned.
- When the outside world reads data through the Watcher, the getter is triggered to add the Watcher to the dependency.
- When the value of an object is changed, the corresponding setter is fired, and the setter notifys each Watcher in the previously dependent collection Dep that its value has changed and the view needs to be rerendered. At this point the Watcher will start calling UPDATE to update the view.
“The last complete responsive code:“
About structure
//defineReactive is a pull away from the Observer const defineReactive = function(obj, key) {
// The following code is omitted }
const Vue = function(options) { console.log("Vue",this) // Print 1 Vue { _data:{ text: "123" Get the text: ƒ the get () setText: ƒset(newVal) }, Mount: ƒ (),Render: ƒ () } // The following code is omitted } const Watcher = function(vm, fn) { console.log("Watcher",this) // prints 3 Watcher this is the object of subs in the Dep below// The following code is omitted } const Dep = function() { console.log("Dep",this) // Print 2 Dep { target: null, subs: [ {// is an instance of Watcher subs: Array(1) 0: Watcher Vm: {// is a Vue instance _data:{ text: "123"// This property has get andsetmethodsGet the text: ƒ the get (), setText: ƒset(newVal) }, Mount: ƒ (),Render: ƒ () }, AddDep: ƒ (dep),Update: ƒ (), value: undefined } ].Depend, ƒ (),AddSub: ƒ (watcher),Notify: ƒ () } // The following code is omitted } const vue = new Vue({ data() { return { text: 'hello world' }; } }) vue.mount(); vue._data.text = '123'; Copy the code
Detailed code
const Observer = function(data) {
Console. log(1) // will be executed when 4 new Vue is started// Loop change to add GET for each attributeset
for (let key in data) {
defineReactive(data, key);
} } const defineReactive = function(obj, key) { Console. log(2) // will be executed when 5 new Vue is started// local variable dep, used for getsetInternal calls const dep = new Dep(); // Get the current value let val = obj[key]; Object.defineProperty(obj, key, { // Sets the current description property to recyclable enumerable: true.// Set the current description property to be modified configurable: true. get() { Console. log(3)// Start 10 Start 19 console.log('in get'); // Call addSub in the dependency collector to collect the dependencies of the current property and Watcher dep.depend(); return val; }, set(newVal) { The console. The log (4) / / 15 if (newVal === val) { return; } val = newVal; // When the value changes, notify the dependent collector, update each Watcher that needs to be updated,// Where does each need to be updated by what determination? dep.subs dep.notify(); } }); } const observe = function(data) { Console. log(5) // will be executed when 3 new Vue is started return new Observer(data); } const Vue = function(options) { Console. log(6)// is executed when 1 new Vue is started const self = this; // Assign data to this._data, the source code part of the Proxy so we use the simplest temporary implementation if (options && typeof options.data === 'function') { Console. log(7)// is executed when 2 new Vue is started this._data = options.data.apply(this); } // Mount function this.mount = function() { Console. log(8) // Start 7 After new Vue, execute vue.mount() new Watcher(self, self.render); } // Render function this.render = function() { Console. log(9) // start 9 Start 18 Render function executed after go here with(self) { _data.text; // When we get data, we use the get method } } / / to monitor this. _data while formingobserve(this._data); // When the new Vue is executed, the new Vue is executed} const Watcher = function(vm, fn) { Console.log (10) // Start 8 Vue. Mount () will take you here const self = this; this.vm = vm; // Point the current dep. target to yourself Dep.target = this; // Add the current Wathcer to the Dep method this.addDep = function(dep) { Console. log(11) // Start 13 dep.addSub(self); } // Update method used to trigger vm._render this.update = function() { The console. The log (12) / / 17 console.log('in watcher update'); fn(); } Vm. _render is called for the first time to trigger get for text// To associate the current Wathcer with the Depthis.value = fn(); // fn is the render function, where fn() is assigned// Dep. Target is cleared to prevent notify from repeatedly binding Watcher to Dep.// Cause code to loop forever Dep.target = null; } const Dep = function() { Console. log(13) // New Dep is executed when 6 new Vue is started, and then executed here const self = this; // Collect the target this.target = null; // Stores the Watcher that needs to be notified in the collector this.subs = []; // Bind the relationship between Dep and Wathcer when there is a target this.depend = function() { Console. log(14) // Start 11 Start 20 Get After obtaining properties, dependency collection will be performed if (Dep.target) { The console. The log (15) / / 12// Self.addSub (dep.target),// I didn't write this because I wanted to restore the source code process. Dep.target.addDep(self); } } // Add Watcher for the current collector this.addSub = function(watcher) { The console. The log (16) / / 14 self.subs.push(watcher); } // Notify all wathcers in the collector to call their update method this.notify = function() { Console. log(17) // Start 16 for (let i = 0; i < self.subs.length; i += 1) { self.subs[i].update(); } } } const vue = new Vue({ data() { return { text: 'hello world' }; } }) vue.mount(); // in get vue._data.text = '123'; // in watcher update /n in get Copy the code
“Resolution:“
-
- We start new Vue, we go to line 46 and execute the Vue constructor, printing 6
-
- The options parameter on line 46 is actually the {data(){}} parameter on line 127, which is an object containing the data function, so options.data is a data function, printing 7. Assign the data returned by the data function in Vue to _data.
-
- And then go to line 67 and Observe is going to go up to line 41 where it defines it.
-
- And then line 43 new Observer will go to the first line
Observer(Key function)
, print 1. We found that the Observer actually adds to dataGet and set
Method, except that the undefined method defineReactive is removed.
- And then line 43 new Observer will go to the first line
-
- Then go to line 9, do defineReactive, print 2, then 15 lines for each
attribute
addGet and set
Methods.
- Then go to line 9, do defineReactive, print 2, then 15 lines for each
-
- And then I go to line 12, and when I do new Dep, I go to line 95 and I do Dep, and I print 13. The rest of the code in the Dep function is just defining the function, it doesn’t execute, it jumps out of the Dep function. It then goes to line 13 of the defineReactive function, and the functions in the rest of the defineReactive code don’t execute either, so it goes back to Observer and back to line 67, i.e
The new Vue process is complete
.
- And then I go to line 12, and when I do new Dep, I go to line 95 and I do Dep, and I print 13. The rest of the code in the Dep function is just defining the function, it doesn’t execute, it jumps out of the Dep function. It then goes to line 13 of the defineReactive function, and the functions in the rest of the defineReactive code don’t execute either, so it goes back to Observer and back to line 67, i.e
-
- Then go to line 135 vue.mount(), go to line 56, and print 8.
-
- Then execute new Watcher to line 70, print 10, and then “DEP. target = this”, which attaches the watch instance to the target property of the Dep, thereby associating it.
-
- Lines 72 through 88 are just definitions, not executed. In line 89 this.value = fn() : fn is actually the “render” function passed in (see line 57), followed by () and executed immediately. Then go to the render function at line 60 and print 9. “Watcher is done,” and then, “Here’s the key” : after printing 9, it goes on, “read _data.text.” So, this step will trigger the GET method (the purpose of this step is just to trigger the GET, so just get the value, nothing else).
-
- Then go to get at line 21 and print 3.
-
- Then go to line 25, execute dep.Depend (), go to line 104, and print 14.
-
- Dep.target is mounted in step 8, which is true. Print 15.
-
- Then go to line 110, skip to line 77, and print 11.
-
- Target = null to avoid falling into an infinite loop. Then Watch completes execution and “vue.mount() also completes execution”.
-
- Then we have line 136 assignment, which goes to line 28 set and prints 4.
-
- Continue down to line 36, dep.notify(), then go to line 119 and print 17.
-
- It then goes to line 122, triggers the update, goes to line 82, and prints 12.
-
- Then execute fn(), the render function, go to line 60 and print 9.
-
- And then you go to line 63, you take data, you go get, you go 21, you print 3.
-
- And then line 25, it jumps to line 104, prints 14. Dep.target is null and 15 will not be printed
Refer to the link
This article is formatted using MDNICE