What can this article do for you? 1, the understanding of the principle of two-way data binding of vue and core code module 2, ease of curiosity at the same time learn how to implement the two-way binding To illustrate the principle and implementation, this article mainly from vue related code source code, and simplified, relatively simple, does not take into account the array processing and data of circular dependencies, and so on, also hard to avoid some problems, Your comments are welcome. However, these will not affect your reading and understanding, I believe that after reading this article for you to read the vue source code will be more helpful < all relevant code in this article can be found on github above github.com/DMQ/mvvm
I believe we should be familiar with MVVM bidirectional binding, a word is not on the code, the following first look at a final implementation of the effect of it, and VUE syntax, if you do not understand bidirectional binding, strong Google
<div id="mvvm-app">
<input type="text" v-model="word">
<p>{{word}}</p>
<button v-on:click="sayHi">change model</button>
</div>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
var vm = new MVVM({
el: '#mvvm-app',
data: {
word: 'Hello World! '
},
methods: {
sayHi: function(a) {
this.word = 'Hi, everybody! '; }}});</script>Copy the code
Effect:
Several ways to implement bidirectional binding
At present, several mainstream MVC (VM) frameworks have implemented one-way data binding, and my understanding of two-way data binding is nothing more than adding change(input) events to inputting elements (input, Textare, etc.) on the basis of one-way binding to dynamically modify model and view, without much depth. So there is no need to worry too much about the implementation of one-way or bidirectional binding.
There are several ways to implement data binding:
Publisher – subscriber mode (backbone.js)
Dirty checking (angular.js)
Data hijacking (vue.js)
Vm. set(‘property’, value) is a common way to update data and view data
This approach is now too low, we prefer to update the data through vm.property = value and update the view automatically, so there are two approaches
Dirty value check: Angular.js uses dirty value detection to determine whether to update views by comparing data changes. The easiest way to do this is to use setInterval() polling to detect data changes. Roughly as follows:
-
DOM events, such as the user entering text, clicking a button, and so on. ( ng-click )
-
XHR response event ($HTTP)
-
Browser Location change event ($Location)
-
Timer event ($timeout, $interval)
-
Perform $digest() or $apply()
Data hijacking: Vue. js adopts the mode of data hijacking combined with the publiser-subscriber mode. Through Object.defineProperty(), it hijacks the setter and getter of each attribute, releases messages to subscribers when data changes, and triggers corresponding listening callback.
Train of thought to sort out
We have learned that VUE uses data hijacking to do data binding. The core method is to hijacking attributes through Object.defineProperty() to monitor data changes. Undoubtedly, this method is one of the most important and basic contents in this article. If you are not familiar with defineProperty, to implement MVVM bidirectional binding, you must implement the following: 1, implement a data listener Observer, can listen to all attributes of the data object, if there is any change can get the latest value and notify the subscriber 2, implement an instruction parser Compile, scan and parse the instructions of each element node, replace the data according to the instruction template. Implement a Watcher as a bridge between the Observer and Compile, subscribe to and receive notification of each property change, execute the corresponding callback function bound by the instruction, update the view, and bind the corresponding MVVM entry function
The above process is shown in the figure:
1. Implement Observer
Let’s do it we know that we can use obeject.defineProperty () to listen for property changes so that we can recursively traverse all data objects that need observe, including properties of child property objects, So if you put a setter and a getter, you assign something to this object, it triggers the setter, and you listen for changes in the data. The relevant code could look like this:
var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; Kindeng --> DMQ
function observe(data) {
if (!data || typeof data! = ='object') {
return;
}
// Retrieve all attributes to traverse
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
};
function defineReactive(data, key, val) {
observe(val); // Listen for child attributes
Object.defineProperty(data, key, {
enumerable: true./ / can be enumerated
configurable: false.// cannot define again
get: function() {
return val;
},
set: function(newVal) {
console.log('Hahaha, I'm listening for a change in value'.val.'- >', newVal);
val= newVal; }}); }Copy the code
So that we can monitor the change of each data, then after listening to change is how to notify the subscriber, so we need to implement a message subscriber, is very simple, maintain an array, used to gather the subscriber, trigger notify data changes, then call the subscriber the update method, code after improvement is this:
/ /... omit
function defineReactive(data, key, val) {
var dep = new Dep();
observe(val); // Listen for child attributes
Object.defineProperty(data, key, {
/ /... omit
set: function(newVal) {
if (val === newVal) return;
console.log('Hahaha, I'm listening for a change in value', val, '- >', newVal);
val = newVal;
dep.notify(); // Notify all subscribers}}); }function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) { sub.update(); }); }};Copy the code
So the question is, who are the subscribers? How do I add a subscriber to a subscriber? Var dep = new dep (); var dep = new dep (); DefineReactive is defined inside the defineReactive method, so adding subscribers via deP must be done inside the closure, so we can do it in the getter:
// Observer.js
/ /... omit
Object.defineProperty(data, key, {
get: function(a) {
// Since we need to add watcher to the closure, we need to define a global target attribute through Dep to temporarily store the watcher and remove it after adding it
Dep.target && dep.addDep(Dep.target);
return val;
}
/ /... omit
});
// Watcher.js
Watcher.prototype = {
get: function(key) {
Dep.target = this;
this.value = data[key]; // The getter for the property is triggered to add a subscriber
Dep.target = null; }}Copy the code
An Observer has been implemented here, complete with the ability to listen to data and notify subscribers of data changes. So the next step is to implement Compile
2. Compile
Compile mainly does is parses the template instructions, replaces the variables in the template with data, initializes the render page view, binds the corresponding node of each instruction to update function, adds the subscriber that listens to the data, receives notification once the data changes, updates the view, as shown in the figure:
Because dom nodes are operated for several times during parsing, in order to improve performance and efficiency, the node EL is first converted into document fragment for parsing and compilation. After parsing, the fragment is added back to the original real DOM node
function Compile(el) {
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
init: function() { this.compileElement(this.$fragment); },
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(), child;
// Copy the native node to the fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
returnfragment; }};Copy the code
The compileElement method will pass through all nodes and their children, scan, parse and compile, call the instruction render function for rendering and call the instruction update function for binding.
Compile.prototype = { // ... Ellipse compileElement: function(el) {var childNodes = el.childNodes, me = this; [].slice.call(childNodes).forEach(function(node) { var text = node.textContent; var reg = /\{\{(.*)\}\}/; If (me. IsElementNode (node)) { me.compile(node); } else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp. $1); } // iterate over the compiler child if (node.childNodes && node.childNodes.length) {
me.compileElement(node); }}); }, compile: function(node) { var nodeAttrs = node.attributes, me = this; [].slice.call(nodeAttrs).foreach (function(attr) {// specify: directives named after v-xxx // e.g<span v-text="content"></span>Var attrName = attr. Name; var attrName = attr. // v-text if (me.isDirective(attrName)) { var exp = attr.value; // content var dir = attrName.substring(2); // text if (me.iseventDirective (dir)) {v-on:click compileutil.eventhandler (node, me.$vm, exp, dir); } else {compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); }}}); }}; Var compileUtil = {text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text'); } / /... Omit the bind: function (node, vm, exp, dir) { var updaterFn = updater[dir + 'Updater']; UpdaterFn && updaterFn(node, vm[exp]); Watcher new watcher (vm, exp, function(value, oldValue) { Update view updaterFn && updaterFn(node, value, oldValue); }); }}; Var updater = {textUpdater: function(node, value) {
node.textContent= typeof value == 'undefined' ? '' : value; } / /... Omit};Copy the code
Recursive traversal ensures that each node and its children are parsed and compiled to the text node, including the {{}} expression declaration. The declaration of directives is marked by node attributes with a specific prefix, such as < SPAN v-text=”content” other-attr v-text is an instruction, and other-attr is not an instruction, but a common attribute. Listening for data and binding updates is handled by adding callbacks to compileutil.bind () via new Watcher() to receive notifications of data changes
At this point, a simple Compile is complete, complete code. The next step is to see Watcher in action as a subscriber
Implement Watcher
Watcher subscribers, acting as a bridge between the Observer and Compile, mainly do the following: Update () = deP () = deP () = deP () = deP () = deP () = deP (); If it’s a bit messy, you can review the previous ideas
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
// Add yourself to the DEp by triggering the getter for the property
this.value = this.get();
}
Watcher.prototype = {
update: function(a) {
this.run(); // Attribute value changes are notified
},
run: function(a) {
var value = this.get(); // Get the latest value
var oldVal = this.value;
if(value ! == oldVal) {this.value = value;
this.cb.call(this.vm, value, oldVal); // Execute the callback bound in Compile to update the view}},get: function(a) {
Dep.target = this; // Point the current subscriber to yourself
var value = this.vm[exp]; // Trigger the getter to add itself to the property subscriber
Dep.target = null; // Set the value
returnvalue; }};// The Observer and Dep are again listed here for easy comprehension
Object.defineProperty(data, key, {
get: function(a) {
// Since we need to add watcher to the closure, we can define a global target attribute in the Dep to temporarily store the watcher and remove it after adding it
Dep.target && dep.addDep(Dep.target);
return val;
}
/ /... omit
});
Dep.prototype = {
notify: function(a) {
this.subs.forEach(function(sub) {
sub.update(); // Call the subscriber's update method to notify the change}); }};Copy the code
When instantiating Watcher, the get() method is called, marking the subscriber to the current Watcher instance with dep.target = watcherInstance, forcing the getter method defined by the property to fire. The current Watcher instance is added to the subscriber DEP of the property so that the watcherInstance is notified of updates when the property value changes.
Ok, Watcher has implemented it as well, complete code. Basically vUE data binding related to the core of several modules are also these several, click here, can be found in the SRC directory vUE source code.
Finally, the logic and implementation of the MVVM entry file are relatively simple
4. Implement MVVM
MVVM, as the entry of data binding, integrates Observer, Compile and Watcher, uses Observer to monitor its model data changes, and uses Compile to parse and Compile template instructions. Finally, Watcher is used to build a communication bridge between Observer and Compile to achieve data change -> view update; View Interactive Changes (INPUT) -> Bidirectional binding effect of data model changes.
A simple MVVM constructor looks like this:
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data;
observe(data.this);
this.$compile = new Compile(options.el || document.body, this)}Copy the code
Var vm = new MVVM({data:{name: ‘kindeng’}}); vm._data.name = ‘dmq’; That’s how you change the data.
Var vm = new MVVM({data: {name: ‘kindeng’}}); vm.name = ‘dmq’;
Therefore, we need to add an attribute proxy method to the MVVM instance, so that the attribute proxy to access vm is the attribute to access vm._data, after the modification code is as follows:
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data, me = this;
XXX -> vm._data. XXX -> vm._data
Object.keys(data).forEach(function(key) {
me._proxy(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
_proxy: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false.enumerable: true.get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) { me._data[key] = newVal; }}); }};Copy the code
Object.defineproperty () is used to hijack the read and write rights of the attributes of the VM instance, so that the read and write attributes of the VM instance are changed to read and write the attribute values of the VM._data
At this point, all modules and functionality are complete, as promised at the beginning of this article. A simple MVVM module has been implemented, most of its ideas and principles from the simplified transformation of Vue source code, you can see all the relevant code in this article here. Because the content of this article is more practical, there is a lot of code, and it is not appropriate to list a large amount of code, so it is suggested that those who want to know more about this article can read the source code again, so that it will be easier to understand and master.
conclusion
This paper mainly focuses on “several ways to achieve bidirectional binding”, “implement Observer”, “Compile”, “implement Watcher”, “implement MVVM” these modules to elaborate the principle and implementation of bidirectional binding. And according to the train of thought process gradually combed explained some details and key content points, as well as by showing part of the key code described how to achieve a two-way binding MVVM step by step. There will certainly be some not rigorous thinking and mistakes in the article, welcome to correct, interested welcome to discuss and improve ~
Finally, thank you for reading!
The front-end footprint