preface


use vue 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 yourself vue Two way binding version of , Let's start with the results chart :

code :                                                                     design sketch :

 


Does it look like vue It's used in the same way ? Next, from principle to implementation , Step by step from simple to difficult SelfVue. 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 version vue The process of , Main implementation {{}},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 , that vue If data hijacking , Let's take a look at the definition of output through the console vue 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 properties a There are two corresponding get and set method , Why are there two more ways ? because vue Yes Object.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 attributes get and set, 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 the ordinary , 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 execute console.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
Property value of . At this time Object.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 called vue Authoritative guide console.log(Book.name); // 《vue Authoritative guide 》 <>
We passed Object.defineProperty(
) Object set Book Of name attribute , For which get and set Rewrite , seeing the name of a thing one thinks of its function ,get Is reading name Function triggered by the value of property ,set Just setting up name 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 called vue Authoritative guide ", And then , When reading this property , It will output
"《vue Authoritative guide 》", Because we are get 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 us vue The data looks a little similar , explain vue This is the way to hijack data . Next, we implement a simple version of the mvvm Two way binding code .

Thinking analysis

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



The key point is data How to update view, because view to update data In fact, it can be monitored through events , such as input 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 , Is through Object.defineProperty(
) Set a set 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 achieve data to update view 了 .



We have ideas , 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 monitor Observer, Used to listen to all properties . If the attribute changes , You need to tell subscribers Watcher See if updates are needed . Because there are many subscribers , So we need to have a message subscriber Dep To collect these subscribers , And then on the monitor Observer And subscribers Watcher Unified management between . next , We also need to have an instruction parser Compile, Scan and parse each node element , Initialize the corresponding instruction into a subscriber Watcher, And replace the template data or bind the corresponding function , When the subscriber Watcher Change of corresponding property received , The corresponding update function will be executed , To update the view . So next we do the following 3 Steps , Realize the two-way binding of data :

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

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

3. Implement a parser Compile, 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. Implement a Observer

Observer It's a data listener , In fact, the core method is as mentioned above Object.defineProperty(
). If you want to listen to all properties , Then you can iterate through all the attribute values by recursion , And carry out Object.defineProperty(
) handle . The following code , Implemented a Observer.
<> 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 , Now the value is :“' + 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 It's been monitored , Now the value is :“vue Authoritative guide ” library.book2 =
' There is no such book '; // attribute book2 It's been monitored , Now the value is :“ There is no such book ” <>

Thinking analysis , You need to create a message subscriber that can hold subscribers Dep, subscriber Dep 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 is list, Put the Observer 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 , Now the value is :“' +
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 subscribe Dep Add a subscriber design in getter inside , This is to make Watcher 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 . stay setter In function , If the data changes , Will notify all subscribers , Subscribers will perform the corresponding updated function . only this and nothing more , A relatively complete Observer It's done , Next, let's design Watcher.

2. realization Watcher


subscriber Watcher You need to add yourself to the subscriber during initialization Dep in , How to add ? We already know the monitor Observer Yes get Function to add subscribers Wather Operating , So we just have to be at the subscriber Watcher When initializing, the corresponding get Function to add subscribers , How does that trigger get Function of , It can't be simpler , As long as the corresponding property value is obtained, it can be triggered , The core reason is because we used Object.defineProperty(
) Data monitoring . There is also a small node to deal with , We just need to be at the subscriber Watcher Only subscribers need to be added during initialization , So we need to make a judgment operation , So you can do something on the subscriber : stay Dep.target Up cache down subscriber , Add it successfully and then remove it . subscriber Watcher 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 the get function Dep.target = null; // Release yourself return value; } }; <>
At this time , We need to monitor Observer Make a little adjustment, too , Mainly corresponding Watcher On class prototypes get function . What needs to be adjusted is defineReactive 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 , Now the value is :“' + newVal.toString() + '”'); dep.notify(); // If the data changes , Notify all subscribers }
}); } Dep.target = null; <>

only this and nothing more , Simple version Watcher Design completed , At this time, all we have to do is Observer and Watcher Connect , You can implement a simple two-way binding data . Because there's no parser yet Compile, So for template data, we do write dead processing , Suppose another node on the template , And id 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 to Observer and Watcher Connect :
<> 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 page new following SelfVue 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 Value changed '); selfVue.data.name = 'canfoo'; }, 2000); </script> <>
This opens the page , You can see that the page just started showing yes 'hello
world', It's over 2s And then it becomes '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 to new
SelfVue Do a proxy processing , Let's visit selfVue Property proxy for is access selfVue.data Properties of , Implementation principle or use Object.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. realization Compile


Although an example of bidirectional data binding has been implemented above , But the whole process has not been resolved dom node , Instead, fix a node directly to replace the data , So next we need to implement a parser Compile For parsing and binding . Resolver Compile Implementation steps :

1. Parsing template instruction , And replace 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 get dom element , And then dom Nodes with instructions on the element , So this link needs to dom Frequent operation , All can be built first fragment fragment , Will need to be resolved dom Node storage fragment We'll deal with it in the clip :
<> function nodeToFragment (el) { var fragment =
document.createDocumentFragment(); var child = el.firstChild; while (child) {
// take Dom Element move in fragment 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 for those 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 for 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 , call compileElement 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 steps 1, Next, you need to generate a subscriber and bind the update function , Corresponding to the above steps 2. This completes the instruction parsing , initialization , Compiling three processes , A parser Compile It will work normally . In order to Compile And monitor Observer And subscribers Watcher Connect , We need to modify the class again SelfVue 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> <>
Above code , Visible on page , at first titile and name Is initialized to 'hello world' And empty ,2s after title Replaced with ' Hello '
3s after name Replaced with '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>现象还没描述?直接上图!!!请观赏



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

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