catalogue
1, Overview of bidirectional binding
1. What is bidirectional binding
2, Implementation of bidirectional binding
1. Implement an Observer (Object.defineProperty())
3. Implement Compile (event listening)
1, Overview of bidirectional binding
1. What is bidirectional binding
Two way binding is a means for Vue and JS to share data;
The methods adopted are:
- Data hijacking
- Publisher subscriber mode
Specifically, Object.defineProperty() is used to hijack setter s and getter s of various properties, publish messages to subscribers when data changes, and trigger corresponding listening callbacks to render the view.
2. Specific steps
- The observer needs to hijack the data and listen to all properties. If there is any change, it will notify the subscriber
Objects are traversed recursively, including the properties of child objects. Setters and getter s are added to the properties. In this way, assigning a value to this object will trigger the setter, and then the data changes can be monitored
- Through watcher (observer), receive the change notification of the attribute and execute the corresponding function to update the view
Watcher (subscriber) is the communication bridge between Observer and Compile. It mainly does:
- When instantiating itself, add yourself to the attribute subscriber (dep)
- Itself must have an update() method
- When the attribute change dep.notice() notification is received, you can call your own update() method and trigger the callback bound in Compile, and you will retire with success.
- Compile (template parser) parses template instructions
Replace the variables in the template with data, then initialize the rendering page view, bind the node corresponding to each instruction to the update function, add the subscriber listening to the data, and update the view after receiving the notification once the data changes
The general process is as follows:
As the entry point of data binding, MVVM integrates Observer, Compile and Watcher, monitors the changes of its model data through Observer, parses the compilation template instructions through Compile, and finally uses Watcher to build a communication bridge between Observer and Compile, so as to achieve the two-way binding effect of data change - > View update; view interactive change (input) - > data model change
Note:
- get and set are keywords and memory properties
- The method corresponding to get is called getter, which is responsible for obtaining values without any parameters. The method corresponding to set is setter, which is responsible for setting values. In its function body, all return s are invalid.
Object.defineProperty() method:
Data hijacking
Syntax:
Object.defineProperty(obj, prop, descriptor)
Parameters:
- obj: Required. Target object;
- prop: Required. The name of the attribute to be defined or modified;
- descriptor: Required. Attributes owned by the target attribute;
Return value:
The object passed into the function, that is, the first parameter obj;
example:
var Book = {} var name = ''; Object.defineProperty(Book, 'name', { set: function (value) { name = value; console.log('You took the title of a book called' + value); }, get: function () { return '<' + name + '>' } }) Book.name = 'vue Authoritative guide'; // You took a book called vue authority guide console.log(Book.name); // vue authoritative guide
2, Implementation of bidirectional binding
Refer to: Two way binding principle and implementation of vue - canfoo#! - blog Garden
1. Implement an Observer (Object.defineProperty())
Observer = (Object.defineProperty() listening + recursion) + subscriber
Observer is a data listener. Its core implementation method is Object.defineProperty(). If you want to listen to all properties, you can traverse all property values through recursive methods and process them with Object.defineProperty(). The following code implements an observer
function defineReactive(data, key, val) { observe(val); // Recursively traverses all child attributes Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { val = newVal; console.log('attribute' + key + 'It has been monitored. The current value is:“' + newVal.toString() + '"'); } }); } function observe(data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }; var library = { book1: { name: '' }, book2: '' }; observe(library); library.book1.name = 'vue Authoritative guide'; // The attribute name has been monitored, and now the value is: "vue authoritative guide" library.book2 = 'There is no such book'; // The property book2 has been monitored, and now the value is: "there is no such book"
In the thought analysis, it is necessary to create a message subscriber Dep that can accommodate subscribers. The subscriber Dep is mainly responsible for collecting subscribers and then executing the update function of the corresponding subscribers when the properties change. Therefore, it is obvious that the subscriber needs to have a container, which is a list. Slightly transform the above Observer and implant it into the message subscriber:
Once the Observer has set trigger, it will notify Dep
function defineReactive(data, key, val) { // observe(val); / / recursively traverse all sub attributes // var dep = new Dep(); Object.defineProperty(data, key, { // enumerable: true, // configurable: true, get: function() { //Added here //We add the subscriber Dep to a subscriber design in the getter, //This is to trigger Watcher initialization, //Therefore, it is necessary to determine whether to add subscribers. As for the specific design scheme, it will be described in detail below. if (Do you need to add subscribers) { dep.addSub(watcher); // Add a subscriber here } return val; }, // set: function(newVal) { // if (val === newVal) { // return; // } // val = newVal; // console.log('attribute '+ key +' has been monitored, and now the value is: "+ newVal.toString() + '"); // dep.notify(); / / notify all subscribers if data changes // } }); } //Here is the subscriber function Dep () { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } };
In the setter function, if the data changes, all subscribers will be notified, and the subscribers will execute the corresponding update function. So far, a relatively complete Observer has been implemented. Next, we start to design the Watcher.
2. Implement Watcher
- Subscriber Watcher needs to add itself to subscriber Dep during initialization
- The listener Observer performs the operation of adding subscriber Wather in the get function, so we just need to trigger the corresponding get function to perform the operation of adding subscriber when the subscriber Watcher is initialized
- There is another fine node that needs to be processed. We only need to add subscribers when subscriber Watcher is initialized, so we need to make a judgment operation. Therefore, we can do something on the subscriber: cache subscribers on Dep.target and remove them after adding successfully. The implementation of subscriber Watcher is as follows:
function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); // Add yourself to the subscriber } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; // Cache yourself var value = this.vm.data[this.exp] // Enforce the get function in the listener Dep.target = null; // Release yourself return value; } };
At this time, we need to make a slight adjustment to the listener Observer, mainly corresponding to the get function on the Watcher class prototype. The adjustment is required in the defineReactive function:
function defineReactive(data, key, val) { // observe(val); / / recursively traverse all sub attributes // var dep = new Dep(); Object.defineProperty(data, key, { // enumerable: true, // configurable: true, get: function() { if (Dep.target) { // Determine whether to add subscribers dep.addSub(Dep.target); // Add a subscriber here } return val; }, // set: function(newVal) { // if (val === newVal) { // return; // } // val = newVal; // console.log('attribute '+ key +' has been monitored, and now the value is: "+ newVal.toString() + '"); // dep.notify(); / / notify all subscribers if data changes // } }); } Dep.target = null;
3. Implement Compile (event listening)
Although an example of two-way data binding has been implemented above, the whole process does not resolve dom nodes, but directly fix a node to replace data. Therefore, next, a parser Compile needs to be implemented to do parsing and binding. The implementation steps of parser Compile are as follows:
- Parse the template instruction, replace the template data, and initialize the view
- Bind the node corresponding to the template instruction to the corresponding update function to initialize the corresponding subscriber
- In order to parse the template, you first need to obtain the dom element, and then process the nodes containing instructions on the dom element