By: Small Potato Blog park: www.cnblogs.com/HouJiao/ Nuggets: juejin.cn/user/243617…
preface
Learn the first vUE source code to talk about the vue2. X two-way data binding source code implementation.
Two-way data binding in VUE is mainly realized through change detection. This article mainly summarizes the change detection of Object.
The implementation principles of Object and Array in vUE bidirectional data binding are different
When we go into an interview, if the candidate’s stack includes the VUE framework, there is a good chance that the interviewer will throw out the question “Do you understand how two-way data binding works in VUE?” I’ve heard some answers, and people can usually come up with a word called publish-subscribe. That’s basically impossible to answer when you ask in depth, or if you can implement a simple two-way data binding for me.
I’ve thrown out three terms at this point: two-way data binding, change detection, and publish-subscribe.
As mentioned earlier, two-way data binding is implemented through change detection. So publish-subscribe is what I understand to be the design philosophy of software, and it goes deeper than change detection, down to the design patterns of the code.
So we can say that bidirectional data binding is implemented through change detection, or we can say that bidirectional data binding is implemented through publish-subscribe. Personally, I don’t think there’s a problem with either, just the way it’s described.
Whether it’s called change detection or publish-subscribe, there are some real life examples to help us understand them.
Many of the following descriptions will use the two terms interchangeably, so you don’t have to worry about how to call them, just know that they’re talking about the same thing
For example, we often play weibo:
A user named KK likes a certain blogger MM very much and then follows her on Weibo. After that, every time the blogger MM published some dynamic eating and drinking on the micro-blog, the micro-blog client will take the initiative to push dynamic to the user KK. After a period of time, the blogger MM broke a bad news, user KK will be the blogger MM micro blog closed.Copy the code
In this actual scenario, we can call the blogger MM a publisher. User KK is a subscriber. The micro blog client is a manager role, it always detect the blogger MM dynamic, when the blogger MM update dynamic push to subscribers.
Based on what we’ve already done, you should have an idea of the publish-subscribe/change-detection design and a few points to focus on:
1. How to detect changes in data (or how to detect content published by publishers) 2. How to collect and save subscribers (that is, how to realize the role of administrator of the micro blog client). 3. How subscribers implement.Copy the code
Then we summarized these points one by one to interpret vue source code implementation.
How do you detect changes in data
If you’ve seen advanced javascript programming, you probably know that the Object class provides a method defineProperty in which you can detect data by defining GET and set.
If you don’t know about Object.defineProperty, go here.
Here is an example of the defineProperty method of Object.
var obj = {};
var name;
Object.defineProperty(obj, 'name', {
enumerable : true.configurable : true.get: function(){
console.log("Get method called");
return name;
},
set: function(newName){
console.log("Set method called"); name = newName; }})// The set method is triggered when the name attribute is modified
obj.name = 'newTodou';
// The get method is triggered when the name attribute is accessed
var objName = obj.name;
Copy the code
We put this code into an HTML, and the console prints the following:
As you can see, when we change the value of the obj.name property, we call the set method of the name property and print “set method called “. When we access the value of the obj.name property, we call the get method of the name property, printing “get method called “.
So this is the answer to the question of how to detect changes in data, isn’t it easy?
Accessing a data attribute value triggers a GET method defined on the attribute; Fires the set method defined on a data attribute when modifying its value. This is a key sentence, and hopefully you'll remember it for a lot of the rest of the lecture.Copy the code
In fact, at this point we can implement a simple two-way data binding: the input field content changes, and the SPAN text content below the input field changes.
Let’s take a look at the whole idea: Listen for the contents of the input box and synchronize the contents of the input box to the innerText property of the SPAN.
We can listen for changes in the input field by using the keyUp event to retrieve the contents of the input field inside the event. That is, we get the changed data, which we store in the name property of an OBJ object.
Synchronizing the contents of the input box to the innerText property of a span is equivalent to synchronizing the changed data to the view. The logic of the update is simple: Spanele.innerText = obj.name.
What we need to consider is where to trigger the update operation.
In the logic that listens for content changes in the input field we said that we’re going to save the changed data toobj.name
In the. So this operation is actually assigning a value to the property of the object, which triggers the property defined on the propertyset
Methods. Synchronizing the contents of the input field to the innerText property of a SPAN naturally falls into the set method of the name property.
At this point, I think it’s pretty easy to write code.
<input type="text" id="name"/>
<br/>
<span id="text"></span>
<script type="text/javascript">
var nameEle = document.getElementById("name");
var textEle = document.getElementById('text');
var obj = {};
Object.defineProperty(obj, 'name', {
enumerable: true.configurable: true.get: function(){
return textEle.value;
},
set: function(newName){
textEle.innerText = newName;
}
})
nameEle.onkeyup = function () {
obj.name = event.target.value;
}
</script>
Copy the code
The whole logic code is relatively simple, but basically enough to deal with the interviewer’s question: can you give me a simple two-way data binding this question.
As we know, an object generally has multiple attributes, and Vue Data generally has multiple or multi-layer attributes and data, such as:
data: {
id: 12091,
context: {
index:1,
result:0
}
}
Copy the code
So we need to make all properties in the object detectable: recursively traverse all properties of the object, defining get and set methods for each property.
The Vue source code encapsulates an Observer class to implement this function.
/* * obj data is actually vue data */
function Observer(obj){
this.obj = obj;
this.walk(obj);
}
Observer.prototype.walk = function(obj) {
// Get all the attributes in obj
var keysArr = Object.keys(obj);
keysArr.forEach(element= >{ defineReactive(obj, element, obj[element]); })}// Refer to the source code to make this method a separate method
function defineReactive(obj, key, val) {
// If obj is an object containing multiple levels of data attributes, each subattribute needs to be recursed
if(typeof val === 'object') {new Observer(val);
}
Object.defineProperty(obj, key,{
enumerable: true.configurable: true.get: function(){
return val;
},
set: function(newVal) { val = newVal; }})}Copy the code
At this point, data detection is complete.
How do I collect and save subscribers
Collecting and saving subscribers is simply a matter of data storage, so don’t worry too much about keeping subscribers in an array.
We talked about the example of weibo earlier:
User KK follows blogger MM and adds a subscriber/element to the array. User Kk takes blogger MM, which can be understood as removing a subscriber/element from the array. The blogger MM releases dynamic information, and the micro blog client actively sends dynamic information to the user KK, which can be understood as notifying the data update operation.Copy the code
The whole thing described above is collecting and saving what subscribers need to care about, which is called how to collect dependencies in vue.js.
So now with that in mind, implement a class Dep, called a subscriber, for managing subscribers/managing dependencies.
function Dep(){
this.subs = [];
}
Dep.prototype.addSub = function(sub){
this.subs.push(sub);
}
// Add dependencies
Dep.prototype.depend = function() {
// What is depObject
// Just for the moment, it's a subscriber/dependency object
this.addSub(depObject);
}
// Remove dependencies
Dep.prototype.removeSub = function(sub) {
// This is done by extracting a remove method from the source code
if(this.subs.length > 0) {var index = this.subs.indexOf(sub);
if(index > -1) {// Notice the use of splice
this.subs.splice(index, 1); }}}// Notify data update
Dep.prototype.notify = function() {
for(var i = 0; i < this.subs.length; i++ ){
// This is equivalent to calling the update method for each element in subs in sequence
// The internal implementation of the update method can be ignored for the moment, but its purpose is to update data
this.subs[i].update()
}
}
Copy the code
With dependency collection and management implemented, we need to consider two questions: when do we add dependencies? When will you be notified to update data?
In the microblog example, user KK follows blogger MM by adding a subscriber/element to the array. That corresponds to code that can be treated as accessing properties of the object, so we can add dependencies when accessing properties of the object.
The blogger MM releases dynamic information, and the micro blog client actively sends dynamic information to the user KK, which can be understood as notifying the data update operation. In the corresponding code, it can be regarded as modifying the property of the object, so we can notify the data update when modifying the property of the object.
This may not be easy to understand, so we can think of what we normally do in VUE: insert data inside the template div tag with double curly braces {{text}}.
This is essentially the same as the div tag in the template reading and relying on the data.text data in the Vue, so we can collect the div as a dependent object. Later, when the text data changes, we need to tell the DIV tag to update its internal data.
With that said, the question of when to add dependencies and when to notify data updates is answered: Add dependencies in GET and notify data updates in set.
Adding dependencies and notifying data updates are functions of the Dep class. The interfaces are dep.Depend and dep.notify, respectively. Now let’s improve the Observer class by adding dependencies in GET and notifying data updates in set.
/* * obj data is actually vue data */
function Observer(obj){
this.obj = obj;
if(Array.isArray(this.obj)){
// If it is an array, the array detection method is called
}else{
this.walk(obj);
}
}
Observer.prototype.walk = function(obj) {
// Get all the attributes in obj
var keysArr = Object.keys(obj);
keysArr.forEach(element= >{ defineReactive(obj, element, obj[element]); })}// Refer to the source code to make this method a separate method
function defineReactive(obj, key, val) {
// If obj is an object containing multiple levels of data attributes, each subattribute needs to be recursed
if(typeof val === 'object') {new Observer(val);
}
var dep = new Dep();
Object.defineProperty(obj, key,{
enumerable: true.configurable: true.get: function(){
// Add dependencies to get
dep.depend();
return val;
},
set: function(newVal) {
val = newVal;
// Notify data update in setdep.notify(); }})}Copy the code
How to achieve subscribers
Again, in the previous Weibo example, user KK is treated as a subscriber and is defined as Watcher in the vue source code. So what does a subscriber have to do?
Let’s review our implementation of the subscriber Dep. The first feature is to add subscribers.
depend() {
// What is depObject
// Just for the moment, it's a subscriber/dependency object
this.addSub(depObject);
}
Copy the code
You can see that the comment in this code at the time is that you can forget about what depObject is and understand for a moment that it’s a subscriber/dependency object.
So now we know that depObject is actually a Watcher instance. So how do you trigger the Depend method to add a subscriber?
When we wrote the code to detect data changes earlier, we triggered the Depend method to add the dependency logic in the property’s GET method.
The design of the vue source code is to trigger the get method of the data attribute on Watcher initialization, which can add the subscriber to the subscriber.
The code is posted below.
/* * vm: vue instance object * exp: attribute name */
function Watcher(vm, exp){
this.vm = vm;
this.exp = exp;
// Trigger the get method of the data attribute at initialization time, that is, the subscriber can be added to the subscriber
this.value = this.get();
}
// Trigger data attribute get method: access data attribute can be implemented
Watcher.prototype.get = function() {
// Access data attribute logic
var value = this.vm.data[this.exp];
return value;
}
Copy the code
Here’s a quick look at the logic of the GET method:
Data attribute access must be achieved by passing the data and the corresponding attribute name. Then consider that the data property in vUE can be accessed using the vue instance object plus the "." operator. So instead of passing in data directly, vUE is designed here to pass in an instance of vue, using vue instance.data [' property name '] to access the property to trigger the property's GET method.Copy the code
Note: Vue also stores accessed data attribute values in the value variable in Watcher.
At this point, the first functionality of Watcher, which follows from the subscriber Dep’s Depend method, is complete: When Watcher initializes, it fires a get method for data attributes, adding subscribers to the subscriber.
Let’s move on to the second function of the subscriber Dep: notification of data updates.
// Notify data update
notify() {
for(let i = 0; i < this.subs.length; i++ ){
// This is equivalent to calling the update method for each element in subs in sequence
// The internal implementation of the update method can be ignored for the moment, but its purpose is to update data
this.subs[i].update()
}
}
Copy the code
The most important line of this code: this.subs[I].update(), which actually triggers the update method of the subscriber Watcher instance. (Because each element in a subs is an instance of a subscriber.) So the second feature of our Watcher is that we need to implement an update function that actually contains the logic to update the data.
So what is the logic of actually updating the data?
Again, vue’s double curly braces example: Use double curly braces {{text}} to insert data inside the template’s div tag.
When the text data changes, the actual logic to update the data is div.innerText = newText. The Update method in Watcher should be familiar.
Going back to the design of vue, it wraps the logic that actually updates the data into a function that is passed to the Watcher constructor when the Watcher instance is initialized and then called in the Update method.
The code implementation is as follows.
/* * VM: vue instance object * exp: property of the object * cb: function that actually contains data update logic */
function Watcher(vm, exp, cb){
this.vm = vm;
this.exp = exp;
this.cb = cb;
// Trigger the get method of the data attribute at initialization time, that is, the subscriber can be added to the subscriber
this.value = this.get();
}
// Trigger data attribute get method: access data attribute can be implemented
Watcher.prototype.get = function() {
// Access data attribute logic
var value = this.vm.data[this.exp];
return value;
}
Watcher.prototype.update = function() {
// When update is triggered, the value of the data attribute is the new value that has been modified
var newValue = this.vm.data[this.exp];
// Trigger the function passed to Watcher to update the data
this.cb.call(this.vm, newValue);
}
Copy the code
The simple update code is implemented, but vue has made a small optimization here.
We access the properties of the data in the get method and save the original value of the data to this.value. So the update method is optimized by comparing this.value with newValue, the old value with the newValue, before executing the update code. The cb function is triggered only if the new value and the old value are not equal.
Watcher.prototype.update = function() {
// When update is triggered, the value of the data attribute is the new value that has been modified
var newValue = this.vm.data[this.exp];
var oldValue = this.value;
if(oldValue ! == newValue){// Trigger the function passed to Watcher to update the data
this.cb.call(this.vm, newValue); }}Copy the code
Cleverly save Watcher instances
We’re missing a bit of logic here. Let’s look at the depend method of the subscriber Dep implemented earlier.
depend() {
// What is depObject
// Just for the moment, it's a subscriber/dependency object
this.addSub(depObject);
}
Copy the code
So this depObject we said it’s a subscriber, an instance of Watcher, so how do you get an instance of Watcher?
Let’s go back and look at the trigger flow for the Depend method:
That is, create a Watcher instance and call the Get method of the Watcher instance to trigger the GET method defined on the data attribute and finally trigger the DEP.Depend method.
So following this process, the Watcher instance must be ready before the GET method defined on the data attribute can be triggered. We know that when we initialize Watcher, the “this” inside Watcher refers to the Watcher instance. So vUE was designed to save the Watcher instance to the Dep target property in the Watcher get method. After the instantiation of Watcher is complete, a global access to dep. target will fetch the Watcher instance.
So now I’m going to supplement the Get method of the Watcher class.
// Trigger data attribute get method: access data attribute can be implemented
Watcher.prototype.get = function() {
// Save the Watcher instance to the Dep target property
Dep.target = this;
// Access data attribute logic
var value = this.vm.data[this.exp];
// Empty the instance
Dep.target = null;
return value;
}
Copy the code
There is a reason to empty the dep. target code in the get method, so read on.
Next we need to complete the Depend method in Dep.
// Add dependencies
Dep.prototype.depend = function() {
// addSub adds a subscriber/dependency object
// The Watcher instance is the subscriber. When the Watcher instance is initialized, it has saved itself to dep.target
if(Dep.target){
this.addSub(Dep.target); }}Copy the code
Now I’m talking about cleaning up the dep.target release code.
If we don’t have dep.target = null, we don’t have if(dep.target) in our Depend method. That is normal after the first subscriber is added. When the data changes, the code executes the logic:
Trigger the set method defined on the data property to execute dep.notify to execute the update method of the Watcher instance....Copy the code
Instead, let’s look at the process of executing the Update method of the Watcher instance.
Watcher.prototype.update = function() {
// When update is triggered, the value of the data attribute is the new value that has been modified
var newValue = this.vm.data[this.exp];
var oldValue = this.value;
if(oldValue ! == newValue){// Trigger the function passed to Watcher to update the data
this.cb.call(this.vm, newValue); }}Copy the code
As you can see, the update method needs to get the new value before executing cb, which actually updates the data. So the data property is accessed again, and as you can imagine, accessing the data property calls the property’s GET method.
And because dep. Depend is executed without any criteria, the current Watcher is implanted in the subscriber twice. This is clearly not normal. Therefore, dep. target = null and if(dep.target) are very necessary steps.
The complete code
Now let’s post the complete code for Observer, Dep, and Watcher.
The Observer to realize the
/* * obj data is actually vue data */
function Observer(obj){
this.obj = obj;
if(Array.isArray(this.obj)){
// If it is an array, the array detection method is called
}else{
this.walk(obj);
}
}
Observer.prototype.walk = function(obj) {
// Get all the attributes in obj
var keysArr = Object.keys(obj);
keysArr.forEach(element= >{ defineReactive(obj, element, obj[element]); })}// Refer to the source code to make this method a separate method
function defineReactive(obj, key, val) {
// If obj is an object containing multiple levels of data attributes, each subattribute needs to be recursed
if(typeof val === 'object') {new Observer(val);
}
var dep = new Dep();
Object.defineProperty(obj, key,{
enumerable: true.configurable: true.get: function(){
// Add dependencies to get
dep.depend();
return val;
},
set: function(newVal) {
val = newVal;
// Notify data update in setdep.notify(); }})}Copy the code
Dep implementation
function Dep(){
this.subs = [];
}
Dep.prototype.addSub = function(sub){
this.subs.push(sub);
}
// Add dependencies
Dep.prototype.depend = function() {
// addSub adds a subscriber/dependency object
// The Watcher instance is the subscriber. When the Watcher instance is initialized, it has saved itself to dep.target
if(Dep.target){
this.addSub(Dep.target); }}// Remove dependencies
Dep.prototype.removeSub = function(sub) {
// This is done by extracting a remove method from the source code
if(this.subs.length > 0) {var index = this.subs.indexOf(sub);
if(index > -1) {// Notice the use of splice
this.subs.splice(index, 1); }}}// Notify data update
Dep.prototype.notify = function() {
for(var i = 0; i < this.subs.length; i++ ){
// This is equivalent to calling the update method for each element in subs in sequence
// The internal implementation of the update method can be ignored for the moment, but its purpose is to update data
this.subs[i].update()
}
}
Copy the code
Watcher implementation
/* * VM: vue instance object * exp: property of the object * cb: function that actually contains data update logic */
function Watcher(vm, exp, cb){
this.vm = vm;
this.exp = exp;
this.cb = cb;
// Trigger the get method of the data attribute at initialization time, that is, the subscriber can be added to the subscriber
this.value = this.get();
}
// Trigger data attribute get method: access data attribute can be implemented
Watcher.prototype.get = function() {
// Save the Watcher instance to the Dep target property
Dep.target = this;
// Access data attribute logic
var value = this.vm.data[this.exp];
// Empty the instance
Dep.target = null;
return value;
}
Watcher.prototype.update = function() {
// When update is triggered, the value of the data attribute is the new value that has been modified
var newValue = this.vm.data[this.exp];
var oldValue = this.value;
if(oldValue ! == newValue){// Trigger the function passed to Watcher to update the data
this.cb.call(this.vm, newValue); }}Copy the code
practice
The key core code has been combed out, the next step is to use.
Because there is no implementation of template compilation, some code needs to be written to death.
To recall the use of two-way data binding in VUE, let’s start with a simple piece of code.
<html>
<head>
<meta charset="utf-8" />
<title>Learn Vue source code -Object change detection</title>
</head>
<body>
<h1>Learn Vue source code -Object change detection</h1>
<div id="box">
{{text}}
</div>
</body>
<script type="text/javascript" src="./Dep.js"></script>
<script type="text/javascript" src="./Observer.js"></script>
<script type="text/javascript" src="./Watcher.js"></script>
<script type='text/javascript'>
/* * data: data * el: element * exp: attribute of the object */
function Vue(data, el, exp){
this.data = data;
this.el = el;
{{text}} values are parsed this way because there is no template-related code
this.innerHTML = this.data[exp];
}
var data = {
text: 'hello Vue'
};
var el = document.getElementById('box');
var vm = new Vue(data, el);
</script>
</html>
Copy the code
After this code runs, the value of {{text}} is displayed in the browser.
It works not because we compiled the template and curly braces, but because we used el.innerhtml = data.text; This is written dead way to achieve.
Next, the first step is to make the data observable by calling an Observer to pass in the data, and we write the code to the Vue constructor.
/* * data: data * el: element * exp: attribute of the object */
function Vue(data, el, exp){
this.data = data;
this.el = el;
this.exp = exp;
{{text}} values are parsed this way because there is no template-related code
this.el.innerHTML = this.data[exp];
// Initializing a VUE instance requires making the data data observable
new Observer(data);
}
Copy the code
Next, you manually create a subscriber for data’s Text property, still in the vue constructor.
Manually creating subscribers is also because there is no template to compile the code, otherwise the normal logic for creating subscribers is to iterate through the template to create subscribers dynamically.
/* * data: data * el: element * exp: attribute of the object */
function Vue(data, el, exp){
this.data = data;
this.el = el;
this.exp = exp;
{{text}} values are parsed this way because there is no template-related code
this.el.innerHTML = this.data[exp];
// Initializing a VUE instance requires making the data data observable
new Observer(data);
this.cb = function(newVal){
this.el.innerHTML = newVal;
}
// Create a subscriber
new Watcher(this, exp, this.cb);
}
Copy the code
The subscriber is created with a cb parameter, which is the function we’ve been talking about that actually contains the logic to update the data.
After that, the last step is to modify the data.text data. If the content of the div changes after the modification, we know that the code has run successfully. The logic to modify data.text is borrowed from a button: listen for the button click event and change the value of data.text to “Hello new vue” when triggered.
<html>
<head>
<meta charset="utf-8" />
<title>Learn Vue source code -Object change detection</title>
</head>
<body>
<h1>Learn Vue source code -Object change detection</h1>
<div id="box">
{{text}}
</div>
<br/>
<button onclick="btnClick()">Click me to change the content of the div</button>
</body>
<script type="text/javascript" src="./Dep.js"></script>
<script type="text/javascript" src="./Observer.js"></script>
<script type="text/javascript" src="./Watcher.js"></script>
<script>
/* * data: data * el: element id * exp: an attribute of the object * cb: a function that actually contains data update logic */
function Vue(data, el, exp){
this.data = data;
this.el = el;
this.exp = exp;
{{text}} values are parsed this way because there is no template-related code
this.el.innerHTML = this.data[exp];
this.cb = function(newVal){
this.el.innerHTML = newVal;
}
// Initializing a VUE instance requires making the data data observable
new Observer(data);
// Create a subscriber
new Watcher(this, exp, this.cb);
}
var data = {
text: 'hello Vue'
};
var el = document.getElementById('box');
var exp = 'text';
var vm = new Vue(data, el, exp);
function btnClick(){
vm.data.text = "hello new vue";
}
</script>
</html>
Copy the code
The code above is the complete code, let’s operate a wave in browse to see the result.
As you can see, our code has run successfully.
That here, VUE bidirectional data binding source code to comb out.
Conclusion: MY vUE source code learning approach will mainly refer to my own just got "simple vue. Js" this book, at the same time will refer to some online content. I will try to summarize what I read from the source code in a more accessible way. If my content helps you, feel free to keep following me or point out the flaws in the comments. At the same time because of the source code learning, so this process I also act as a source porter role, do not create code only to carry and interpret the source code.Copy the code
Write in the last
If this article has helped you, please follow ❤️ + like ❤️ and encourage the author
Article public number first, focus on unknown treasure program yuan for the first time to get the latest article
Ink ❤ ️ ~