The lion in front of the city
A long way can be completed step by step. And then a short road, do not stride feet can not reach.
Vue bidirectional binding principle and implementation
preface
Use VUE or have a period of time, although the principle of two-way binding also have a general understanding, but also did not have a good exploration of the principle of implementation, so this time specially spent a few nights to consult information and read the relevant source code, oneself also realize a simple version of vUE two-way binding version, first the results to attract you:
Code: Renderings:
Does it look similar to how vue is used? The next step is to implement this SelfVue step by step from principle to implementation, from simple to difficult. Since this article is just for learning and sharing, so it is just a simple implementation of the principle, and not too much consideration of the situation and design, if you have any suggestions, welcome to put forward.
This paper mainly introduces two main contents:
1. Vue data bidirectional binding principle.
2. The implementation of simple version of vUE process, mainly to achieve {{}}, V-Model and event instruction function.
Related code address: github.com/canfoo/self…
Vue bidirectional data binding principle
Vue bidirectional data binding is implemented by data hijacking in combination with the publisher/subscriber pattern. If vUE is data hijacking, we can first look at what it means to output an object defined on the vUE initialization data through the console.
Code:
var vm = new Vue({ data: { obj: { a: 1 } }, created: function () { console.log(this.obj); }});Copy the code
Results:
We can see that property A has two corresponding get and set methods. Why are there two more methods? This is because vue implements data hijacking via object.defineProperty ().
What is object.defineProperty () used for? It controls specific operations on an object’s properties, such as the ability to read and write, and whether or not it can be enumerated. Let’s first look at its two description properties, get and set. If you are not familiar with them, please click here to read more about them.
In normal times, it’s easy to print out the property data of an object:
Var Book = {name: 'vue authoritative guide '}; console.log(Book.name); // Vue authoritative guideCopy the code
What if I wanted to execute console.log(book.name) and add a book title to the title of the book? Or what to listen for for the value of the property of the object Book. This is where object.defineProperty () comes in handy, as follows:
var Book = {} var name = ''; Object.defineProperty(Book, 'name', { set: function (value) { name = value; Console. log(' you took a title called '+ value); }, get: function () {return ' '+ name +' '}}) book.name = 'vue '; // You took a definitive guide called vue console.log(book.name); // The Authoritative Guide to VUECopy the code
We set the name property of the Book Object with object.defineProperty () and override the get and set properties. As the name suggests, get is reading the name value and set is setting the name value. So when we execute book.name = ‘vue authoritative guide ‘, the console prints “you took a title called vue Authoritative Guide”, and then, when we read the property, it prints “VUE authoritative Guide”, because we processed the value in the get function. If at this point we execute the following statement, what will the console output?
console.log(Book);Copy the code
Results:
At first glance, it looks like we printed vUE data on it, indicating that vUE does hijack data in this way. Next, we implement a simple version of MVVM bidirectional binding code through the principles.
Thought analysis
MVVM implementation mainly includes two aspects, data changes update view, view changes update data:
The key is how the data updates the View, because the view can actually update the data by listening for events, such as the input tag listening for the ‘input’ event. So let’s focus on how to update the view when the data changes.
The point of the data update view is how to know that the data has changed, and once you know that the data has changed, then everything else is easy to deal with. How do we know that the data has changed? We’ve already given the answer by setting a set function on the property of object.defineProperty (), which is triggered when the data has changed. So we just need to put some methods in there to update the data to update the view.
With the idea, the next step is the implementation process.
The implementation process
We already know that to implement bidirectional binding of data, we first need to hijack the data, so we need to set up a listener Observer to listen for all properties. If the property has changed, you need to tell the subscriber Watcher to see if it needs to be updated. Because there are many subscribers, we need to have a message subscriber (Dep) to collect these subscribers and manage them uniformly between the listener Observer and the subscriber Watcher. Then, we also need a directive parser Compile, which scans and parses each node element, initialises the corresponding instructions to a subscriber Watcher, and replaces template data or binds corresponding functions. At this time, when the subscriber Watcher receives the changes of corresponding attributes, it will execute the corresponding update function. To update the view. Therefore, we will perform the following three steps to achieve bidirectional data binding:
1. Implement a listener Observer that hijacks and listens for all properties and notifies subscribers if there are changes.
2. Implement a subscriber Watcher that receives notification of property changes and executes corresponding functions to update the view.
3. Implement a parser Compile, which can scan and parse the relevant instructions of each node, and initialize the corresponding subscribers according to the initialization template data.
The flowchart is as follows:
1. Implement an Observer
Observer is a data listener, and the core implementation method is the object.defineProperty () mentioned above. If you want to listen for all properties, you can recursively iterate over all property values and handle them with object.defineProperty (). The following code implements an Observer.
function defineReactive(data, key, val) { observe(val); Object.defineproperty (data, key, {enumerable: true, signals: true, get: function() {return val; // Loop through all sub-properties recurse.object.defineProperty (data, key, {enumerable: true, signals: true, get: function() {return val; }, set: function(newVal) { val = newVal; Console. log(' property '+ key +' has been listened on and now has the value: "' + 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 name attribute has been listened on and now has the value: "Vue authoritative guide" library.book2 = 'no book '; // The property book2 has been listened on and now has the value "no book".Copy the code
In the analysis, we need to create a message subscriber Dep that can hold subscribers. The subscriber Dep is mainly responsible for collecting subscribers and then executing the update function for subscribers when the property changes. So obviously the subscriber needs a container, which is a list, and the above Observer is slightly modified to embed it in the message subscriber:
function defineReactive(data, key, val) { observe(val); Var dep = new dep (); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: Function () {if (need to add subscribers) {dep.addsub (watcher); // Add a subscriber here} return val; }, set: function(newVal) { if (val === newVal) { return; } val = newVal; Console. log(' property '+ key +' has been listened on and now has the value: "' + newval.tostring () + '"); dep.notify(); // If the data changes, 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
From the code, we designed the subscriber Dep to add a subscriber in the getter, this is for the Watcher initialization to trigger, so we need to determine whether to add a subscriber, as detailed in the design scheme, later. In the setter, if the data changes, all subscribers will be notified, and subscribers will perform the corresponding update function. At this point, a fairly complete Observer has been implemented, so let’s start designing the Watcher.
2. Realize the Watcher
The subscriber Watcher needs to add itself to the subscriber Dep at initialization time. How does that work? We already know that the listener Observer adds the subscriber Wather when the get function is initialized, so we just need to start the corresponding get function to add the subscriber when the Watcher is initialized. How to trigger the get function is as simple as possible. Just get the corresponding property value to trigger, the core reason is that we used object.defineProperty () for data listening. One more detail is that we only need to add subscribers when the subscriber Watcher is initialized, so we need to make a decision, so we can do something on the subscriber: cache the subscriber on the dep. target, add the subscriber successfully and then remove it. 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(); } 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; Var value = this.vm.data[this.exp] var value = this.vm.data[this.exp] Return value; }};Copy the code
At this point, we also need to make a slight adjustment to the listener Observer, which corresponds to the get function on the Watcher class prototype. Where you need to tweak is in the defineReactive function:
function defineReactive(data, key, val) { observe(val); 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(' property '+ key +' has been listened on and now has the value: "' + newval.tostring () + '"); dep.notify(); // If the data changes, notify all subscribers}}); } Dep.target = null;Copy the code
At this point, the simple version of Watcher is designed, and we can implement a simple bidirectional binding of data by associating the Observer with the Watcher. Because there is no parser designed here, so we do write dead processing for template data, assuming that there is another node on the template, and the ID number is ‘name’, and the bound variable is also ‘name’, and it is wrapped in two large double parentheses (here just for disguise, it is not useful for the moment). The template is as follows:
<body>
<h1 id="name">{{name}}</h1>
</body>Copy the code
At this point we need to associate the Observer with the Watcher:
function SelfVue (data, el, exp) { this.data = data; observe(data); el.innerHTML = this.data[exp]; New Watcher(this, exp, function (value) {el.innerhtml = value; }); return this; }Copy the code
Then on the page, new the following SelfVue class is used to implement bidirectional binding of data:
<body> <h1 id="name">{{name}}</h1> </body> <script src="js/observer.js"></script> <script src="js/watcher.js"></script> <script src="js/index.js"></script> <script type="text/javascript"> var ele = document.querySelector('#name'); var selfVue = new SelfVue({ name: 'hello world' }, ele, 'name'); Window.settimeout (function () {console.log('name value changed '); selfVue.data.name = 'canfoo'; }, 2000); </script>Copy the code
When you open the page, you can see that the page starts with ‘Hello World’, and after 2 seconds it becomes ‘canfoo’. So that’s half the work, but there’s a little bit of detail here, because we’re assigning in the form ‘selfvue.data.name = ‘canfoo’ and the ideal form is’ selfvue.name = ‘canfoo’ and in order to do that, We need to do a proxy processing when new SelfVue, so that the property proxy accessing SelfVue is to access the property of selfvue.data. The implementation principle is to use object.defineProperty () to package another layer of property values:
function SelfVue (data, el, exp) { var self = this; this.data = data; Object.keys(data).forEach(function(key) { self.proxyKeys(key); // Bind proxy properties}); observe(data); el.innerHTML = this.data[exp]; New Watcher(this, exp, function (value) {el.innerhtml = value; }); return this; } SelfVue.prototype = { proxyKeys: function (key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function proxyGetter() { return self.data[key]; }, set: function proxySetter(newVal) { self.data[key] = newVal; }}); }}Copy the code
Then we can change the template data directly with the form ‘selfvue. name = ‘canfoo’. If you want to see the phenomenon urgently children shoes hurry up to get the code!
3. Realize the Compile
The above example of bidirectional data binding has been implemented, but the process is not to parse the DOM node, but to fix a node to replace the data, so the next step is to implement a parser Compile to do the parsing and binding work. Compile parser implementation steps:
1. Parse the template directive, replace the template data, and initialize the view
2. Bind the node corresponding to the template directive to the corresponding update function and initialize the corresponding subscriber
In order to parse the template, we first need to obtain the DOM element, and then process the node containing instructions on the DOM element. Therefore, this link requires frequent DOM operations. So we can build a fragment first, and store the DOM node to be parsed in the fragment fragment before processing:
function nodeToFragment (el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; While (child) {// move the Dom element into the fragment fragment.appendChild(child); child = el.firstChild } return fragment; }Copy the code
The next step is to iterate through the nodes and do special processing for the nodes that contain the relevant designations. Here we will deal with the simplest case first, only with the form ‘{{variable}}’. We will consider more cases later:
function compileElement (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{(.*)\}\}/; var text = node.textContent; If (self.istextnode (node) && reg.test(text)) {// check whether the command is in {{}} form self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); // Continue recursively through the child nodes}}); }, function compileText (node, exp) { var self = this; var initText = this.vm[exp]; this.updateText(node, initText); New Watcher(this.vm, exp, function (value) {// generate the subscriber and bind the update function self.updatetext (node, value); }); }, function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }Copy the code
After fetching the outermost node, call compileElement to judge all child nodes. If the node is a text node and matches the {{}} directive, compile the node. Compile the view data first, which corresponds to step 1 above. The next step is to generate and bind a subscriber to the update function, as described in Step 2 above. In this way, the three processes of instruction parsing, initialization and compilation are completed, and a parser Compile can work normally. To associate the parser Compile with the listener Observer and the subscriber Watcher, we need to modify the SelfVue function again:
function SelfVue (options) {
var self = this;
this.vm = this;
this.data = options;
Object.keys(this.data).forEach(function(key) {
self.proxyKeys(key);
});
observe(this.data);
new Compile(options, this.vm);
return this;
}Copy the code
<body> <div id="app"> <h2>{{title}}</h2> <h1>{{name}}</h1> </div> </body> <script src="js/observer.js"></script> <script src="js/watcher.js"></script> <script src="js/compile.js"></script> <script src="js/index.js"></script> <script type="text/javascript"> var selfVue = new SelfVue({ el: '#app', data: { title: 'hello world', name: '' } }); Window.settimeout (function () {selfvue.title = 'hello '; }, 2000); window.setTimeout(function () { selfVue.name = 'canfoo'; }, 2500); </script>Copy the code
In the code above, you can see that the titile and name are initialized to ‘Hello World’ and null, respectively. After 2 seconds, the title is replaced by ‘Hello’ and after 3 seconds, the name is replaced by ‘canfoo’. No more nonsense, and give you a version of the code (V2), get the code!
At this point, a data bidirectional binding function has been basically completed, the next step is to improve the parsing and compilation of more instructions, where to do more instructions processing? The obvious answer is to add another directive node to compileElement, then iterate through all its attributes to see if there are any matching directive attributes, and if there are, parse and compile them. Here we add a v-model directive and a parse compilation of the event directive. We use the compile function to parse these nodes:
function compile (node) { var nodeAttrs = node.attributes; var self = this; Array.prototype.forEach.call(nodeAttrs, function(attr) { var attrName = attr.name; if (self.isDirective(attrName)) { var exp = attr.value; var dir = attrName.substring(2); If (self.iseventDirective (dir)) {// the event directive self.compileEvent(node, self.vm, exp, dir); } else {// v-model command self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); }}); }Copy the code
The above compile function is mounted on the compile prototype, it first traverses all node attributes, then determines whether the attributes are directive attributes, if so, then distinguishes which instructions, and then performs corresponding processing, the processing method is relatively simple, so it is not listed here. Those of you who want to read the code right now can click here and get it right away.
Finally, let’s remodel SelfVue a little bit to make it more like vue:
function SelfVue (options) { var self = this; this.data = options.data; this.methods = options.methods; Object.keys(this.data).forEach(function(key) { self.proxyKeys(key); }); observe(this.data); new Compile(options.el, this); options.mounted.call(this); // Execute mounted after everything is done}Copy the code
At this point we can really test this by setting the following things on the page:
<body> <div id="app"> <h2>{{title}}</h2> <input v-model="name"> <h1>{{name}}</h1> <button v-on:click="clickMe">click me! </button> </div> </body> <script src="js/observer.js"></script> <script src="js/watcher.js"></script> <script src="js/compile.js"></script> <script src="js/index.js"></script> <script type="text/javascript"> new SelfVue({ el: '#app', data: { title: 'hello world', name: 'canfoo' }, methods: { clickMe: function () { this.title = 'hello world'; Mounted: function () {window.settimeout () => {this.title = 'hello '; }, 1000); }}); </script>Copy the code
Does it look like vue is used the same way, ha, really done! If you want the code, click here to get it! The phenomenon has not been described? Directly above!! Please watch the
In fact, this effect picture, is the beginning of this article to post the effect picture, the previous said to take you to achieve, so here the picture again, this is called the first and last echo.
Finally, I hope this article is helpful to you. If you have any questions, please leave a message to discuss together.