Preface


Usevue It's been a while, Although I have a general understanding of its two-way binding principle, But I haven't explored its principle, So I spent a few nights looking up the materials and reading the relevant source code, Realize a simple version by yourselfvue Two way binding version of, Let's start with the results chart:

Code:                                                                     Design sketch:

 


Does it look likevue It's used in the same way? Next, from principle to implementation, Step by step from simple to difficultSelfVue. Because this article is just for learning and sharing, So it's just a simple principle, Not considering too many situations and designs, If you have any suggestions, Welcome to bring it up.

This paper mainly introduces two main contents:

1. vue The principle of data bidirectional binding.

2. Implementation of simple versionvue Process, Main realization{{}},v-model And event command functions.

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

vue Principle of data bidirectional binding


vue Data bi-directional binding combines publishers through data hijacking- Subscriber mode, thatvue If data hijacking, Let's take a look at the definition of output through the consolevue What is the object on initialization data.

Code:
<> var vm = new Vue({ data: { obj: { a: 1 } }, created: function () {
console.log(this.obj); } }); <>
Result:



We can see the propertiesa There are two correspondingget andset Method, Why are there two more ways? becausevue It is throughObject.defineProperty() To achieve data hijacking.

Object.defineProperty(
) What is it for? It can control some specific operations of an object property, For example, reading and writing rights, Can I enumerate, Here we mainly study the two corresponding description attributesget andset, If you are not familiar with its usage,
Please click here for more usage
<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty>
.

In common, We can easily print out the attribute data of an object:
var Book = { name: 'vue Authoritative guide' }; console.log(Book.name); // vue Authoritative guide
If you want to executeconsole.log(book.name) At the same time, Add a book name directly to the title, How to deal with it? Or what kind of monitoring object to pass Book
Attribute value. This timeObject.defineProperty( ) It's going to work, The code is 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 Authoritative guide'; //
You took a title calledvue Authoritative guide console.log(Book.name); // 《vue Authoritative guide》 <>
We passedObject.defineProperty(
) Object setBook Ofname attribute, For themget andset Rewrite, Seeing the name of a thing one thinks of its function,get Is readingname Function triggered by the value of property,set Just setting upname Function triggered by the value of property, So when Book.name
= 'vue Authoritative guide' When this statement, The console will print out " You took a title calledvue Authoritative guide", Then, When reading this property, Will export
"《vue Authoritative guide》", Because we areget This value is processed in the function. If we execute the following statement at this time, What will the console output?
console.log(Book);
Result:




At first glance, Do you want to print it with usvue The data looks a little similar, Explainvue This is the way to hijack data. Next, we implement a simple version of themvvm Two way binding code.

Thinking analysis

Realizationmvvm It mainly includes two aspects, Data change update view, View change update data:



The key point isdata How to updateview, becauseview To updatedata In fact, you can use event monitoring, such asinput Tag monitoring 'input'
Events can be implemented. So let's focus on the analysis, When data changes, How to update the.


The focus of the data update view is how to know that the data has changed, As long as you know the data has changed, So the next thing is easy to deal with. How to know the data has changed, In fact, we have given the answer above, It is throughObject.defineProperty(
) Set aset function, When the data changes, it triggers this function, So we just need to put some methods that need to be updated in this to achievedata To updateview 了.



There is a train of thought. The next step is the realization process.

Implementation process


We already know how to implement two-way data binding, First, hijack and monitor the data, So we need to set up a monitorObserver, Used to listen to all properties. If the attribute changes, You need to tell subscribersWatcher See if updates are needed. Because there are many subscribers, So we need to have a message subscriberDep To collect these subscribers, And then on the monitorObserver And subscribersWatcher Unified management between. Next, We also need to have an instruction parserCompile, Scan and parse each node element, Initialize the corresponding instruction into a subscriberWatcher, And replace the template data or bind the corresponding function, When the subscriberWatcher Change of corresponding property received, The corresponding update function will be executed, To update the view. So next we do the following3 One step, Realize the two-way binding of data:

1. Implement a monitorObserver, Used to hijack and listen to all properties, If there is any change, Notify subscribers.

2. Implement a subscriberWatcher, You can receive notification of property changes and perform the corresponding functions, To update the view.

3. Implement a parserCompile, Can scan and parse the instructions of each node, And initialize the corresponding subscriber according to the initialization template data.

The flow chart is as follows:



1. Achieve oneObserver

Observer It's a data listener, In fact, the core method is as mentioned aboveObject.defineProperty(
). If you want to listen to all properties, Then you can iterate through all the attribute values by recursion, And carry outObject.defineProperty(
) Handle. The following code, Implemented aObserver.
<> function defineReactive(data, key, val) { observe(val); // Recursively traversing all child properties
Object.defineProperty(data, key, { enumerable: true, configurable: true, get:
function() { return val; }, set: function(newVal) { val = newVal;
console.log(' attribute' + key + ' It's been monitored, 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'; // attributename It's been monitored, The value is now:“vue Authoritative guide” library.book2 =
' There is no such book'; // attributebook2 It's been monitored, The value is now:“ There is no such book” <>

Thinking analysis, You need to create a message subscriber that can hold subscribersDep, subscriberDep Mainly responsible for collecting subscribers, Then the update function of the corresponding subscriber is executed when the attribute changes. So obviously the subscriber needs to have a container, This container islist, Top upObserver Slightly improved, Implant message subscriber:
<> function defineReactive(data, key, val) { observe(val); // Recursively traversing all child properties 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(' attribute' + key + ' It's been monitored, The value is now:“' +
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(); }); } }; <>

From the code point of view, We will subscribeDep Add a subscriber design ingetter inside, This is to makeWatcher Initiate to trigger, So you need to decide if you want to add subscribers, As for the specific design scheme, It will be explained in detail below. staysetter Function inside, If the data changes, Will notify all subscribers, Subscribers will perform the corresponding updated function. Only this and nothing more, A relatively completeObserver It's done, Next, let's designWatcher.

2. RealizationWatcher


SubscriberWatcher You need to add yourself to the subscriber during initializationDep in, How to add? We already know the monitorObserver Is inget Function to add subscribersWather Operational, So we just have to be at the subscriberWatcher When initializing, the correspondingget Function to add subscribers, How does that triggerget Function, It can't be simpler, As long as the corresponding property value is obtained, it can be triggered, The core reason is because we usedObject.defineProperty(
) Data monitoring. There is also a small node to deal with, We just need to be at the subscriberWatcher Only subscribers need to be added during initialization, So we need to make a judgment operation, So you can do something on the subscriber: stayDep.target Up cache down subscriber, Add it successfully and then remove it. SubscriberWatcher The implementation is as follows:
<> function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp =
exp; this.value = this.get(); // Actions to 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 theget function Dep.target = null; // Release yourself return value; } }; <>
This time, We need to monitorObserver Make a little adjustment, too, Mainly correspondingWatcher On class prototypesget function. What needs to be adjusted isdefineReactive function:
<> function defineReactive(data, key, val) { observe(val); // Recursively traversing all child properties var
dep = new Dep(); Object.defineProperty(data, key, { enumerable: true,
configurable: true, get: function() { if (Dep.target) {. // Determine if subscribers need to be added
dep.addSub(Dep.target); // Add a subscriber here } return val; }, set: function(newVal) {
if (val === newVal) { return; } val = newVal; console.log(' attribute' + key +
' It's been monitored, The value is now:“' + newVal.toString() + '”'); dep.notify(); // If the data changes, Notify all subscribers }
}); } Dep.target = null; <>

Only this and nothing more, Simple EditionWatcher Finished design, At this time, all we have to do isObserver andWatcher Link up, You can implement a simple two-way binding data. Because there's no parser yetCompile, So for template data, we do write dead processing, Suppose another node on the template, Andid be entitled as'name', And the variable of the binding of the two-way binding is also'name', And it's wrapped in two big brackets( It's just a cover up, It's not useful for the time being), The template is as follows:
<body> <h1 id="name">{{name}}</h1> </body>
At this time, we need toObserver andWatcher Link up:
<> function SelfVue (data, el, exp) { this.data = data; observe(data);
el.innerHTML = this.data[exp]; // Initialize values for template data new Watcher(this, exp, function
(value) { el.innerHTML = value; }); return this; } <>
Then on the pagenew FollowingSelfVue class, You can realize the two-way 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 The value has changed.'); selfVue.data.name = 'canfoo'; }, 2000); </script> <>
This opens the page, You can see that the page just started showing yes'hello
world', Past2s And then become'canfoo'了. Come here, It's half done, But there's one more detail, When we assign values, it's like this '
 selfVue.data.name = 'canfoo'  ' And our ideal form is'  selfVue.name = 'canfoo'
 ' In order to achieve this form, We need tonew
SelfVue Do a proxy processing, Let visitselfVue Property proxy for is accessselfVue.data Attribute, Implementation principle or useObject.defineProperty(
) Wrap the attribute value one more layer:
<> function SelfVue (data, el, exp) { var self = this; this.data = data;
Object.keys(data).forEach(function(key) { self.proxyKeys(key); // Binding agent properties });
observe(data); el.innerHTML = this.data[exp]; // Initialize values for template data 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; } }); } } <>
Now we can go through it directly'  selfVue.name = 'canfoo'  ' To change the template data. If you want to see the phenomenon of children's shoes, quickly get the code!
<https://github.com/canfoo/self-vue/tree/master/v1>

3. RealizationCompile


Although an example of two-way data binding has been implemented above, But the whole process has not been resolveddom node, Instead, fix a node directly to replace the data, So next we need to implement a parserCompile For parsing and binding. ParserCompile Implementation steps:

1. Parsing template instruction, And replace the template data, Initialize view

2. Bind the node corresponding to the template instruction to the corresponding update function, Initialize the corresponding subscriber


To parse a template, First, you need to getdom element, And thendom Nodes with instructions on the element, So this link needs todom Frequent operation, All can be built firstfragment fragment, Will need to be resolveddom Node storagefragment We'll deal with it in the clip:
<> function nodeToFragment (el) { var fragment =
document.createDocumentFragment(); var child = el.firstChild; while (child) {
// takeDom Element migrationfragment in fragment.appendChild(child); child = el.firstChild }
return fragment; } <>
Next, we need to traverse each node, Special treatment for nodes with relevant designations, Let's deal with the simplest situation first, Only with '{{ variable}}'
This form of instruction is processed, It's hard to simplify first, Consider more instructions 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)) { // To judge whether it conforms to this form{{}} Instructions self.compileText(node,
reg.exec(text)[1]); } 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 subscriber and bind update function self.updateText(node, value); }); }, function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value; } <>

After getting the outermost node, callcompileElement function, Judge all child nodes, If the node is a text node and matches{{}} The nodes of this form of instruction begin to compile, First of all, the view data needs to be initialized for compilation, Corresponding to the above steps1, Next, you need to generate a subscriber and bind the update function, Corresponding to the above steps2. This completes the instruction parsing, Initialization, Compiling three processes, A parserCompile It will work normally. In order toCompile And listenerObserver And subscribersWatcher Link up, We need to modify the class againSelfVue function:
<> 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 change, Let's not bind in two directions by passing in fixed element values, You can name all kinds of variables and bind them in two directions: <> <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> <>
Code as above, Visible on page, At firsttitile andname Is initialized to 'hello world' Empty space,2s aftertitle Be replaced ' Hello'
3s aftername Be replaced 'canfoo' 了. Not much nonsense, I'll give you another version of this code(v2), Get code!
<https://github.com/canfoo/self-vue/tree/master/v2>


Come here, A bi-directional data binding function has been basically completed, The next step is to improve the parsing and compiling of more instructions, Where can more instructions be processed? The answer is obvious, As long as上文说的compileElement函数加上对其他指令节点进行判断,然后遍历其所有属性,看是否有匹配的指令的属性,如果有的话,就对其进行解析编译.这里我们再添加一个v-model指令和事件指令的解析编译,对于这些节点我们使用函数compile进行解析处理:
<> 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)) { // 事件指令
self.compileEvent(node, self.vm, exp, dir); } else { // v-model 指令
self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); }
}); } <>

上面的compile函数是挂载Compile原型上的,它首先遍历所有节点属性,然后再判断属性是否是指令属性,如果是的话再区分是哪种指令,再进行相应的处理,处理方法相对来说比较简单,这里就不再列出来,想要马上看阅读代码的同学可以马上
点击这里获取. <https://github.com/canfoo/self-vue/tree/master/v3>

最后我们在稍微改造下类SelfVue,使它更像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); // 所有事情处理好后执行mounted函数 } <>
这时候我们可以来真正测试了,在页面上设置如下东西:
<> <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 = '你好'; }, 1000); } }); </script> <>
是不是看起来跟vue的使用方法一样,哈,真正的大功告成!想要代码,直接点击这里获取!
<https://github.com/canfoo/self-vue/tree/master/v3>现象还没描述?直接上图!!!请观赏



其实这个效果图,就是本文开头贴出来的效果图了,前文说着要带着大家实现,所以就在这里把图再贴一次了,这叫首尾呼应嘛.

最后希望本文对你有帮助,如果有问题请留言一起探讨.