Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

The principle and implementation of two-way binding of vue

2025-01-16 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Network Security >

Share

Shulou(Shulou.com)06/01 Report--

Preface

It has been good to use vue for some time. Although I have a general understanding of its bi-directional binding principle, I have not explored its implementation, so I have spent several nights consulting materials and reading relevant source codes. I have also implemented a simple bi-directional binding version of vue. First, I have a result chart to attract you:

Code: effect picture:

Does it look similar to the way vue is used? Next, from principle to implementation, from simple to difficult to implement this SelfVue step by step. As this article is only for learning and sharing, so it is only a simple implementation of the principle, and did not consider too much situation and design, if you have any suggestions, welcome to put forward.

This paper mainly introduces two major contents:

1. The principle of bidirectional binding of vue data.

two。 The process of realizing the simple version of vue, mainly realizing the functions of {{}}, v-model and event instructions.

Related code address: https://github.com/canfoo/self-vue

Principle of two-way binding of vue data

Two-way binding of vue data is realized through data hijacking combined with publisher-subscriber mode, so if vue is hijacked, we can first take a look at what it is to output an object defined on vue initialization data through the console.

Code:

Var vm = new Vue ({data: {obj: {a: 1}}, created: function () {console.log (this.obj);}})

Results:

We can see that attribute a has two corresponding get and set methods. Why are these two more methods? Because vue implements data hijacking through Object.defineProperty ().

What is Object.defineProperty () used for? It can control some specific operations of an object attribute, such as read and write rights, whether it can be enumerated, here we first study its corresponding two description attributes get and set, if you are not familiar with its usage, please click here to read more usage.

In general, we can easily print out the property data of an object:

Var Book = {name: 'vue authoritative Guide'}; console.log (Book.name); / / vue authoritative Guide

What if you want to add a title number directly to the title while executing console.log (book.name)? Or what to listen for the property value of the object Book. At this point, Object.defineProperty () comes in handy, with the following code:

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 authoritative Guide'; / / you chose a title called vue authoritative Guide console.log (Book.name) / / "authoritative Guide to vue"

We set the name property of the object Book through Object.defineProperty () and rewrite its get and set. As the name implies, get is the function triggered by reading the value of the name property, and set is the function triggered by setting the value of the name property, so when the statement Book.name = 'vue authoritative Guide' is executed, the console will print out, "you have taken a book called vue authoritative Guide", followed by When reading this property, the "vue authoritative Guide" is output because we processed the value in the get function. What will the console output if we execute the following statement at this time?

Console.log (Book)

Results:

At first glance, it looks a little similar to what we printed vue data on it, indicating that vue does hijack data in this way. Next, we use its principle to implement a simple version of the mvvm two-way binding code.

Train of thought analysis

The implementation of mvvm mainly includes two aspects: data changes to update views, and view changes to update data:

The key point is how data updates view, because view updating data can actually be done through event listening, 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 focus of the data update view is how to know that the data has changed, as long as you know that the data has changed, then the rest is easy to deal with. How to know that the data has changed, in fact, we have given the answer above, that is, through Object.defineProperty () to set a set function for the property, when the data changes, it will trigger this function, so we just need to put some methods that need to be updated in it to achieve data update view.

With the idea, the next step is to realize the process.

Realization process

We already know that to achieve two-way data binding, the first thing to do is to hijack and listen to the data, so we need to set up a listener Observer to listen for all properties. If the property changes, you need to tell the subscriber Watcher to see if it needs to be updated. Because there are many subscribers, we need a message subscriber Dep to specifically collect these subscribers and then manage them uniformly between the listener Observer and the subscriber Watcher. Next, we need an instruction parser Compile, which scans and parses each node element, initializes the relevant instructions into a subscriber Watcher, and replaces the template data or binds the corresponding function. When the subscriber Watcher receives the change of the corresponding attribute, it will execute the corresponding update function to update the view. So next we perform the following three steps to achieve two-way data binding:

1. Implement a listener Observer that hijacks and listens to all properties and notifies subscribers of any changes.

two。 Implement a subscriber Watcher that can update the view by receiving notification of property changes and executing the corresponding functions.

3. Implement a parser Compile, which can scan and parse the relevant instructions of each node, and initialize the corresponding subscriber according to the initialization template data.

The flow chart is as follows:

1. Implement an Observer

Observer is a data listener, and its core implementation method is the aforementioned Object.defineProperty (). If you want to listen for all attributes, you can iterate through all property values using recursive methods and Object.defineProperty () on them. The following code implements an Observer.

Function defineReactive (data, key, val) {observe (val); / / Recursively traverse all sub-attributes Object.defineProperty (data, key, {enumerable: true, configurable: true, get: function () {return val;}, set: function (newVal) {val = newVal) Console.log ('attribute' + key + 'has been monitored and the value is now: "' + 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'; / / attribute name has been monitored. Now the value is: "vue authoritative Guide" library.book2 ='No this book'; / / attribute book2 has been monitored, now the value is: "No this book"

In the idea 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 executes the update function of the corresponding subscribers when the properties change. So obviously the subscriber needs to have a container, which is list. Modify the above Observer slightly and insert the message subscriber:

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 (whether you need to add subscribers) {dep.addSub (watcher) / / add a subscriber} return val;} here, set: function (newVal) {if (val = newVal) {return;} val = newVal Console.log ('attribute' + key + 'has been monitored, and the value is now: "' + newVal.toString () +'"); dep.notify (); / / notify all subscribers if the data changes}});} function Dep () {this.subs = [];} Dep.prototype = {addSub: function (sub) {this.subs.push (sub) }, notify: function () {this.subs.forEach (function (sub) {sub.update ();});}}

From a code point of view, we will add a subscriber to the subscriber Dep design in getter, which is to let Watcher initialization trigger, so we need to determine whether to add subscribers, as for the specific design, the following will be described in detail. In the setter function, if the data changes, all subscribers will be notified and the subscribers will execute the corresponding updated function. So far, a relatively complete Observer has been implemented, and then we begin to design the Watcher.

two。 Implement Watcher

The subscriber Watcher needs to add itself to the subscriber Dep during initialization, so how do I add it? We already know that the listener Observer performs the operation of adding the subscriber Wather in the get function, so we just need to start the corresponding get function to add the subscriber when the subscriber Watcher is initialized. It is easy to trigger the get function, as long as you get the corresponding attribute value, the core reason is that we use Object.defineProperty () for data monitoring. There is another detail to deal with here. We only need to add a subscriber when the subscriber Watcher is initialized, so we need to do a judgment operation, so we can do something on the subscriber: cache the subscriber on the Dep.target, and then delete it after the subscriber is successfully added. 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 Dep.target = null in the listener / / release yourself return value;}}

At this point, we need to make a slight adjustment to the listener Observer, mainly corresponding to the get function on the prototype of the Watcher class. What needs to be adjusted is 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 you need to add a subscriber dep.addSub (Dep.target); / / add a subscriber} return val;} here, 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 the data changes};} Dep.target = null

At this point, the simple version of Watcher has been designed, and at this point we can implement a simple two-way binding of data by associating Observer with Watcher. Since there is no parser Compile designed here, we all write to the template data, assuming that there is another node on the template, and the bound variable whose id number is' name', and bound in both directions is also 'name', and is wrapped in two curly braces (just to cover up here, it is of no use for the time being). The template is as follows:

`name`

At this point, we need to associate Observer with Watcher:

Function SelfVue (data, el, exp) {this.data = data; observe (data); el [XSS _ clean] = this.data [exp]; / / initialize the value of template data new Watcher (this, exp, function (value) {el [XSS _ clean] = value;}); return this;}

Then new the following SelfVue classes on the page to achieve two-way data binding:

`name` 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)

When you open the page, you can see that at the beginning of the page, it shows that 'hello world', becomes' canfoo' after 2 seconds. At this point, we are half done, but there is one more detail. When we assign values, we have the form 'selfVue.data.name =' canfoo' 'and our ideal form is' selfVue.name = 'canfoo''. In order to implement this form, we need to do a proxy processing at the time of new SelfVue, so that the property agent that accesses the selfVue accesses the properties of the selfVue.data. The principle of implementation is to use Object.defineProperty () to wrap another layer of attribute 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 [XSS _ clean] = this.data [exp] / / initialize the value of template data new Watcher (this, exp, function (value) {el [XSS _ clean] = 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;}});}}

Now we can change the template data directly in the form of 'selfVue.name =' canfoo''. If you want to see the phenomenon of children's shoes quickly to get the code!

3. Implement Compile

Although the above has implemented an example of two-way data binding, the whole process does not parse the dom node, but directly fix a node to replace the data, so next you need to implement a parser Compile to do the parsing and binding work. Parser Compile implementation steps:

1. Parse the template instruction and replace the template data to initialize the view

two。 Bind the node corresponding to the template instruction to the corresponding update function and 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. Therefore, this step requires frequent dom operations, so you can first create a fragment fragment and store the dom nodes that need to be parsed into the fragment fragment before processing:

Function nodeToFragment (el) {var fragment = document.createDocumentFragment (); var child = el.firstChild; while (child) {/ / move Dom elements into fragment fragment.appendChild (child); child = el.firstChild} return fragment;}

Next, we need to traverse each node and carry out special processing for the nodes with relevant assignments. Here, we will first deal with the simplest case. We will only deal with instructions in the form of'{{variable}}'. It is difficult to simplify first, and more instructions will be considered 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)) {/ / determine whether it conforms to the instruction self.compileText (node, reg.exec (text) [1]) of this form {{}};} if (node.childNodes & & node.childNodes.length) {self.compileElement (node); / / continue recursively traversing child nodes}) }, function compileText (node, exp) {var self = this; var initText = this.vm [exp]; this.updateText (node, initText); / / initialize the initialized data into the view 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;}

After obtaining the outermost node, call the compileElement function to judge all the child nodes. If the node is a text node and matches the {{}} instruction, the compilation process begins. The compilation process first needs to initialize the view data. Corresponding to step 1 mentioned above, you need to generate a subscriber and bind the update function, corresponding to step 2 mentioned above. In this way, the parsing, initialization and compilation of instructions 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-like 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;}

After the change, instead of bi-directional binding by passing in fixed element values, we can name various variables for bi-directional binding:

`title``name` 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)

As in the code above, you can see on the page that at first titile and name are initialized to 'hello world' and empty, respectively, and after 2s title is replaced by' Hello'3s and name is replaced by 'canfoo''. No more nonsense, and then give you this version of the code (v2), get the code!

At this point, a two-way data binding function has been basically completed, and the next step is to improve the parsing and compilation of more instructions. where can more instructions be processed? The answer is obvious: just add the compileElement function mentioned above to determine the other instruction nodes, and then iterate through all its attributes to see if there are any matching instruction attributes, and if so, parse and compile them. Here we add a parsing compilation of v-model instructions and event instructions. For these nodes, we use the function compile for parsing processing:

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)) {/ / event instruction self.compileEvent (node, self.vm, exp, dir);} else {/ / v-model instruction self.compileModel (node, self.vm, exp, dir);} node.removeAttribute (attrName);}});}

The above compile function is mounted on the Compile prototype, it first traverses all node attributes, and then determines whether the attribute is an instruction attribute, and then distinguishes which instruction it is, and then processes it accordingly. The processing method is relatively simple, and it is no longer listed here. Students who want to read the code immediately can click here to get it.

Finally, we modify the class SelfVue slightly to make it more like the use of 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 function} after everything is done

At this point, we can actually test it by setting up the following things on the page:

`title``name` click me! 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);}})

Does it look like the way vue is used? ha, it's really done! If you want the code, just click here to get it! The phenomenon has not been described yet? Go straight to the picture! Please watch.

In fact, this effect picture is the effect map posted at the beginning of this article. The previous article said that we should take everyone to realize it, so I will paste the picture again here, which is called echoing from beginning to end.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Network Security

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report