With Angular, React, and Vue competing on the front end, you can’t stand on the front end unless you choose one side, or even two or three.
So we should always be curious and embrace change. Only in constant change can you be invincible.
Recently, I have been studying Vue, and I only have a limited understanding of its bidirectional binding. In recent days, I plan to study in depth, and have some understanding of its principle through several days of learning and consulting materials. Therefore, I have written an example of bidirectional binding by myself, and let’s see how it is implemented step by step.
After reading this article I am sure you will have a clear understanding of Vue’s bidirectional binding principle. It also helps us understand Vue better.
Look at the renderings first
<div id="app">
<input v-model="name" type="text">
<h1>{{name}}</h1>
</div>
<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>
const vm = new Mvue({
el: "#app",
data: {
name: "I am Modern."}}); </script>Copy the code
Data binding
Before we start, let’s talk about data binding. Data binding, as I understand it, is to expose data M (Model) to view V (View). Common architectural modes include MVC, MVP and MVVM. At present, the front-end framework basically adopts MVVM mode to achieve bidirectional binding, and Vue is no exception. However, each framework implements bidirectional binding in slightly different ways, and there are currently about three implementations.
- Publish and subscribe model
- Angular’s dirty check mechanism
- The data was hijacked
Vue uses a combination of data hijacking and publish and subscribe to achieve bidirectional binding, and data hijacking is mainly implemented through Object.defineProperty.
Object.defineProperty
In this article we won’t go into detail about the use of Object.defineProperty. We’ll focus on its accessor properties GET and set. Let’s take a look at what happens to the object properties set with it.
var people = {
name: "Modeng",
age: 18
}
people.age; //18
people.age = 20;
Copy the code
The above code is just plain old getting/setting the properties of the object.
var modeng = {}
var age;
Object.defineProperty(modeng, 'age', {
get: function () {
console.log("Age of acquisition");
return age;
},
set: function (newVal) {
console.log("Set age"); age = newVal; }}); modeng.age = 18; console.log(modeng.age);Copy the code
You’ll notice that after doing this, we automatically execute get when we access the age property and set when we set the age property, which makes bidirectional binding very convenient.
Analysis of the
We know that the MVVM pattern is about keeping the data in sync with the view, meaning that the view is automatically updated when the data changes, and the view updates the data when it changes.
So what we need to do is how do we detect changes in the data and tell us to update the view, how do we detect changes in the view and update the data. Detection view this is relatively simple, but we can use the event listening.
So how do you know if the data properties have changed? This takes advantage of the Object.defineProperty we talked about above, which automatically fires the set function to notify us to update the view when our property changes.
implementation
From the above description and analysis, we know that Vue realizes bidirectional binding through data hijacking combined with publish and subscribe mode. We also know that data hijacking is done through the Object.defineProperty method, and once we know this, we need a listener Observer to listen for property changes. We need a Watcher subscriber to update the view knowing that the properties have changed, and we also need a compile directive parser to parse our node element directives and initialize the view. So we need the following:
- Observer listener: Used to listen for changes in properties to notify subscribers
- Watcher subscriber: Receives property changes and then updates the view
- Compile parser: Parses instructions, initializes templates, and binds subscribers
So let’s go step by step with this idea.
Monitor the Observer
The purpose of the listener is to listen for every property of the data. We also mentioned above that using object.defineProperty, we need to notify the Watcher subscriber to perform an update function to update the view when we are listening for property changes. In this process we may have many subscribers Watcher so we are going to create a container Dep to do a unified management.
functionDefineReactive (data, key, value) {// Call recursively, listen for all attributes observer(value); var dep = new Dep(); Object.defineProperty(data, key, { get:function () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set: function (newVal) {
if(value ! == newVal) { value = newVal; dep.notify(); // Notification subscriber}}}); }function observer(data) {
if(! data || typeof data ! = ="object") {
return;
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
});
}
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
}
Dep.prototype.notify = function () {
console.log('Property change tells Watcher to perform update view function');
this.subs.forEach(sub => {
sub.update();
})
}
Dep.target = null;
Copy the code
Having created a listener Observer, we can now see what happens when we add a listener to an object and change its properties.
var modeng = {
age: 18
}
observer(modeng);
modeng.age = 20;
Copy the code
We can see that the browser console prints “property change notify Watcher to execute update view function” indicating that the listener we implemented is fine. Now that we have a listener, we can notify Watcher of property change, so it’s definitely time to need Watcher.
The subscriber Watcher
Watcher basically accepts notification of property changes, and then executes the update function to update the view, so what we do is basically two steps:
- Add Watcher to the Dep container, where we use the listener’s get function
- After receiving the notification, the update function is executed.
function Watcher(vm, prop, callback) {
this.vm = vm;
this.prop = prop;
this.callback = callback;
this.value = this.get();
}
Watcher.prototype = {
update: function () {
const value = this.vm.$data[this.prop];
const oldVal = this.value;
if(value ! == oldVal) { this.value = value; this.callback(value); } }, get:function() { Dep.target = this; // Store the subscriber const value = this.vm.$data[this.prop]; Target = null; Dep. Target = null; Dep.returnvalue; }}Copy the code
Now that we’ve got Watcher out of the way, we’ve implemented a simple bidirectional binding, and we can try to combine the two to see what happens.
function Mvue(options, prop) {
this.$options = options;
this.$data = options.data;
this.$prop = prop;
this.$el = document.querySelector(options.el);
this.init();
}
Mvue.prototype.init = function () {
observer(this.$data);
this.$el.textContent = this.$data[this.$prop];
new Watcher(this, this.$prop, value => {
this.$el.textContent = value;
});
}
Copy the code
Here we try to use an instance to pass in data and the properties we want to listen on, listen on the data through a listener, then add a property subscription and bind the update function.
<div id="app">{{name}}</div>
const vm = new Mvue({
el: "#app",
data: {
name: "I am Modern."}},"name");
Copy the code
We can see that the data is normally displayed on the page, so we will modify the data through the console, and the rear view will also be modified as changes occur.
At this point we have basically implemented a simple bidirectional binding, but it is not hard to notice that the properties are written dead and there is no instruction template parsing, so the next step is to implement a template parser.
Compile the parser
Compile is used to parse instructions to initialize templates, to add subscribers, and to bind update functions.
Because we frequently manipulate DOM in the process of parsing DOM nodes, we use Document fragments to help us parse DOM to optimize performance.
function Compile(vm) {
this.vm = vm;
this.el = vm.$el;
this.fragment = null;
this.init();
}
Compile.prototype = {
init: function () {
this.fragment = this.nodeFragment(this.el);
},
nodeFragment: function (el) {
const fragment = document.createDocumentFragment();
letchild = el.firstChild; // Move all the child nodes to the document fragmentwhile (child) {
fragment.appendChild(child);
child = el.firstChild;
}
returnfragment; }}Copy the code
Then we need to process and compile the entire node and instructions, call different rendering functions for different nodes, bind update functions, and add DOM fragments to the page after compilation.
Compile.prototype = {
compileNode: function (fragment) {
let childNodes = fragment.childNodes;
[...childNodes].forEach(node => {
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if(this.isElementNode(node)) { this.compile(node); // Render command template}else if (this.isTextNode(node) && reg.test(text)) {
let prop = RegExp.The $1; this.compileText(node, prop); // Render {{}} template} // Recursively compile child nodesif(node.childNodes && node.childNodes.length) { this.compileNode(node); }}); }, compile:function (node) {
let nodeAttrs = node.attributes;
[...nodeAttrs].forEach(attr => {
let name = attr.name;
if (this.isDirective(name)) {
let value = attr.value;
if (name === "v-model") { this.compileModel(node, value); } node.removeAttribute(name); }}); }, // omit... }Copy the code
Because the code is relatively long if all posted will affect the reading, we mainly talk about the whole process to achieve the idea, the end of the article I will send out the source code, interested can go to view all the code.
Here we have completed the whole template compilation, but here we did not implement too much instruction, we just simple implementation of the V-Model instruction, the intention is through this article to let everyone familiar with and understand the principle of Vue two-way binding, not to create a new MVVM instance. So there’s not a lot of detail and design.
Now that we have implemented Observer, Watcher, and Compile, the next step is to organize all three together into a complete MVVM.
Create Mvue
Here we create an Mvue class (constructor) to host Observer, Watcher, and Compile.
function Mvue(options) {
this.$options = options;
this.$data = options.data;
this.$el = document.querySelector(options.el);
this.init();
}
Mvue.prototype.init = function () {
observer(this.$data);
new Compile(this);
}
Copy the code
Then we test the results to see if our implementation of Mvue really works.
<div id="app">
<h1>{{name}}</h1>
</div>
<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>
const vm = new Mvue({
el: "#app",
data: {
name: "No problem, doesn't it look cool?"}}); </script>Copy the code
$data.name = vm.$data.name = vm.$data.name = vm. In fact, it is very simple, Vue does a data broker operation.
Data brokers
Let’s modify Mvue to add the data proxy function. We also use the Object.defineProperty method to perform an intermediate conversion operation to access the data indirectly.
function Mvue(options) {
this.$options = options;
this.$data = options.data;
this.$el= document.querySelector(options.el); // Data broker object.keys (this).$data).forEach(key => {
this.proxyData(key);
});
this.init();
}
Mvue.prototype.init = function () {
observer(this.$data);
new Compile(this);
}
Mvue.prototype.proxyData = function (key) {
Object.defineProperty(this, key, {
get: function () {
return this.$data[key]
},
set: function (value) {
this.$data[key] = value; }}); }Copy the code
At this point we can modify our properties just like Vue, perfect. It’s completely do-it-yourself, and you should try it, and experience the fun of writing your own code.
conclusion
- This paper is mainly on the Vue bidirectional binding principle of learning and implementation.
- It is mainly the study of the whole idea, and did not take into account too many implementation and design details, so there are still many problems, not perfect.
- Source code address, the whole process of the entire code, I hope to help you.
- If you find this article helpful, please share and like it.