Our first thoughts on the vue.js stack may be that it is easy to get started and friendly to beginners. Indeed, when THE author just started with it, he felt it was easier, and in the process of using it, he also felt its power.

Recently in preparation for the interview, only know the use of vue.js is far from enough, so began to analyze vue.js source code. The following steps explain the principle and implementation.

Give the github address of the source code first.

Understand vue.js

Official: Vue.js is a set of progressive frameworks for building user interfaces. So how to understand “progressive”?

Vue’s core function is a view template engine, but that doesn’t mean Vue can’t be a framework. As you can see below, this includes all the parts of Vue. Based on declarative rendering (view template engine), we can build a complete framework by adding component systems, client routing, and large-scale state management. What’s more, these functions are independent of each other, and you can build on top of the core function with whatever parts you want, not all of them. It can be seen that the “progressive” is actually the way Vue is used, and also reflects the design concept of Vue

Incremental means not doing more than you’re supposed to.

Vue.js provides only the most core component systems and two-way data binding (also known as data-driven) in the VUE-CLI ecosystem.

Principle and implementation of bidirectional data binding

Note: because the implementation of this function uses ES6 syntax and some commonly used knowledge, the third part mainly explains the knowledge points used, you can first understand the basic knowledge and then look at the implementation of this part.

We all know that the two core components of vue.js are the component system and the data driver (two-way data binding), so let’s talk about two-way data binding and its implementation.

  1. Implement a Compile, parse the instructions, initialize the view, subscribe to changes in the data, and bind the update function
  2. Implement an Observer that hijacks data and notifies it of changes
  3. Implement a Watcher that acts as a mediator between the two, allowing the Dep to add the current Watcher while accepting data changes and notifying the view to update in time
  4. Implement MVVM, integrating all three as an entry function

To better understand the implementation process, post a more detailed flow chart:

The following explanation is divided into three parts: template Compiler, Data hijacking (Observer), and Watcher.

We all know that we need new Vue({}) when we use Vue. In this case, Vue is a class, and the contents passed inside the big (curly) brackets are Vue properties and methods. The most basic elements needed for bidirectional data binding are EL and data. With the compilable template and data, we can do subsequent template compilation and data hijacking, and finally update the view by monitoring the data changes in real time through the observer. So the Vue class acts as a bridge between template compilation and data hijacking.

Because the following code and the content are for the author’s own implementation of the function as an example, so the Vue class was renamed to MVVM, the basic function is the same (the following code is not complete, the main purpose is to explain the main process and main function).

1. Template Compiler

Class Compile {//vm--> the second argument passed to MVVM is an instance of MVVM, i.e. New MVVM() constructor(el, vm) {// The second argument passed to MVVM is an instance of MVVM#app or document.getelementById ('app')this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; // Prevent the user from entering anything other than"The #el "string is also not a Document node
        if(this.el) {// If this element is available, we will compile //1. First move the real DOM into memory (for better performance) -> use node fragmentationletfragment = this.nodeToFragment(this.el); Compile => Extract the desired element node (V-model) and text node {{}} this.compile(fragment) //3. AppendChild (fragment)}} Put compiled fragment back on the page this.el.appendChild(fragment)}}Copy the code

Once you’ve determined that you have a compilable template, the next three steps are:

1.1 Move the real DOM into memory

Manipulating DOM nodes directly can be very performance destructive, especially for a real page or project, where there are many nodes and nested nodes in the DOM layer, so you can expect performance to suffer if you manipulate DOM directly. Here we use fragment node fragmentation to reduce performance issues caused by direct and extensive DOM manipulation. (This process can be simply understood as moving the DOM nodes into memory and performing a series of operations on the DOM nodes in memory to improve performance.)

NodeToFragment (el) {// The contents of the EL need to be put into memory // document fragments, not real DOM, are nodes in memorylet fragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = el.firstChild) {// Move the actual nodes in el into the document fragment one by one. AppendChild (firstChild); }returnfragment; // Nodes in memory}Copy the code

The above code is the process of moving the DOM node into memory. After executing the above code, open the browser console and you will find that the previous node has been sent (saved into memory).

1.2 Compilation =>(Extract element nodes and text nodes to be compiled)

Compile (fragment) {// Requires recursionletchildNodes = fragment.childNodes; Array.from(childNodes). ForEach (node => {ifThis.iselementnode (node)) {this.iselementNode (node)) {this.iselementNode (node)) {this.iselementNode (node)); This.compile (node) this.compile(node) this.compile(node) this.compile(node) this.compile(node) this.compile(node) this.compile(node) this.compile(node) this.compile(node) this.compile(node) this.compile(node)elseThis.piletext (node)}})} compileElement(node) {// compileElement(node) {this.piletext (node)}}letattrs = node.attributes; Array.from(attrs).foreach (attr => {// Check whether the attribute name contains v-let attrName = attr.name;
            if(this.isDirective(attrName)) {// Take the corresponding value (i.e., message from data (example)) and place it in the nodelet expr = attr.value;
                let [, type] = attrName.split(The '-'// Node this.vm.$dataExpr // There may be V-model or V-text and there may be V-html (only the first two here) CompileUtil[type] (node, enclosing the vm, expr)}})} compileText (node) {/ / compile with {{}}letexpr = node.textContent; // Take the contents of the textlet reg = /\{\{([^}]+)\}\}/g;
        if (reg.test(expr)) {
            //node this.vm.$data expr
            CompileUtil['text'](node, this.vm, expr)
        }
    }
Copy the code

The result of the above three functions is to get the element node that needs to be compiled, and finally to display the corresponding data from the passed data on the template.

TextContent = value}, modelUpdater(node, value) {node.value = value}Copy the code

1.3 The compiled fragment is put back on the page

After displaying the data on the node, we find that there is no data displayed on the page and the element node does not exist. That is because we did all the above operations in memory, and finally we need to put the compiled fragment back on the page.

 this.el.appendChild(fragment)
Copy the code

At this point, the template compilation part is complete, and we should see that the data we defined in data is fully rendered on the page.

Next, we continue to implement data hijacking

2. Data Hijacking (Observer)

As the name implies, data hijacking is monitoring every attribute value in data and doing something about it whenever the data changes (in this case, updating the view). Without saying more, first post code, in which a few points to note

Class Observer {constructor(data) {this. Observer (data)} Observer (data) {//setAnd getif(! data || typeof data ! = ='object') {// If the data does not exist or is not an objectreturn; Key => {forEach(key => {forEach(key => {forEach(key => {)); DefineReactive (data, key, data[key]); // Deep recursive hijacking, where the recursion only hijacks the data in the original data (addsetAnd get methods), if used in the defineReactive functionsetThis. Observer (data[key]); // Define defineReactive(obj, key, value) {// Define defineReactive(obj, key, value)let that = this;
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true.get() {// The method to call when the value is evaluatedreturn value;
            },
            set(newValue) {// Change the value of the acquired property when setting a value in the data propertyif(newValue ! == value) { console.log(this,'this'); Observer (newValue); // This refers to the modified value. // But this is not an instance of Observer, so we need to initially save this to that. Observer (newValue); // If the object continues to hijack value = newValue; }}})} /** *Copy the code

The data hijacking part is simpler and mainly uses Object.defineProperty(). Here’s one caveat:

  • Change the original property in data togetandsetPreviously, data needs to be judged to exclude non-objects and data does not exist
  • Because the data in data can be multi-layered nested objects, deep recursion is required, but the recursion here only hijacks the original data in data, not the newly added data.
  • Based on the defect of the above article, we need to add the datasetMethod (this refers to the modified value, so save the current this value at the beginning of the method)

The core template compilation and data hijacking are complete, and both parts can perform their own functions, but how to link the two together to achieve the ultimate bidirectional binding effect?

Here’s the home of Watcher combining the two!!

3. Watcher

Create a Watcher observer, compare the new value with the old value, and call the update method to update the view if it changes.

class Watcher { constructor(vm, expr, cb) { this.vm = vm; this.expr = expr; this.cb = cb; This.value = this.get(); } getVal(vm, expr) {expr = expr.split('. ');
        return expr.reduce((prev, next) => { //vm.$data.a....
            return prev[next];
        }, vm.$data)}get() { Dep.target = this; // Put the current watcher instance into tartgetlet value = this.getVal(this.vm, this.expr);
        Dep.target = null;
        returnvalue; } // External exposure methodupdate() {
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value;
        if(newValue ! == oldValue) { this.cb(newValue); // Callback for watch}}}Copy the code

Insert a knowledge publish-subscribe model here:

//observer.js /** * publish subscribe */ class Dep {constructor() {this.subs = []; } // Add subscriber addSub(watcher) {this.subs.push(watcher); } / / notificationnotify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}
Copy the code

Publish subscriptions are here to stay: Because Watcher is here to watch data change, namely subscribers. Because one data may be used in multiple places in the template, one data can have multiple monitors. It can be understood that there are multiple subscribers for a data, so when a data changes, all subscribers can be notified of the result of the message in a single notification. (I.e., a change in the value of the data used in the template)

The above paragraph is my understanding at the present stage, if there is any mistake, I hope you can put forward, and make joint improvement and efforts ~~~

By combining the above two functions, you can tie together the entire data bidirectional binding:

When creating Watcher instances in template compilation, this line of code dep.target = this; Dep. Target = null; Dep. Target = null; Dep. In this step, we first put all the subscribers into the subscriber array.

Callback new Watcher(vm, expr, (newValue) => {// Callback new Watcher(vm, expr, (newValue)) () updateFn && updateFn(node, this.getVal(vm, expr))})Copy the code
//observer.js // define defineReactive(obj, key, value) {// Define defineReactive(obj, key, value) {// Define defineReactive(obj, key, value)let that = this;
        console.log(that, this);
        
        letdep = new Dep(); Object. DefineProperty (obj, key, {enumerable:}) {obj.defineProperty (obj, key, {enumerable:});true,
            configurable: true.get() {// Call the method dep.target && dep.addSub(dep.target) when the value is set;return value;
            },
            set(newValue) {// Change the value of the acquired property when setting a value in the data propertyif(newValue ! == value) { console.log(this,'this'); Observer (newValue); // This refers to the modified value. // But this is not an instance of Observer, so we need to initially save this to that. Observer (newValue); // If the object continues to hijack value = newValue; dep.notify(); // Notify everyone that the data is updated}}})}Copy the code

Define an array in the data hijacking section dep.target && dep.addSub(dep.target); To store the subscribers that need to be updated.

When we get the value, we put all of these subscribers into the array defined above, dep.target && dep.addSub(dep.target);

When the value is changed, dep.notify() is called; // Notify everyone that the data is updated, indirectly calling watcher.update() to update the data.

At this point, two-way data binding is basically implemented, and there are two simple things to note.

Add a click event to the input field

// Add the click event node.adDeventListener ('input', e => {
            let newValue = e.target.value;
            this.setVal(vm, expr, newValue);
        })
Copy the code

Add the agent

If we want this. Message to access data, we need to use a layer of proxy. If we want this.

    proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(newValue) {
                    data[key] = newValue
                }
            })
        })
    }
Copy the code

The final result

Three, prepare knowledge

Analysis of vue.js source code process found that vue.js used a lot of the bottom of our peacetime code implementation is not how to use the knowledge, here as a simple list of entry.

1. Classes in ES6 and their definitions

Problem 1: traditional JS class definition

The traditional way to define a class in JS is through a constructor that defines and generates new objects. The Prototype property gives you the ability to add properties and methods to an object.

Case study:

//Person.js
function Person(x,y){    
    this.x = x;   
    this.y = y;
}

Person.prototype.toString = function() {return (this.x + "The age is." +this.y+"Age");
}

export {Person};

//index.js
import {Person} from './Person';
let person = new Person('Joe', 12); console.log(person.toString()); / Zhang SAN's age is 12Copy the code

Question 2: Definition of classes in ES6

ES6 introduces the concept of Class as a template for objects. Classes can be defined using the Class keyword. Basically, ES6 classes can be seen as a syntactic candy that does most of what ES5 does. The new Class writing method simply makes object prototype writing clearer and more like object-oriented programming syntax. The above code is rewritten using ES6 “classes”, which look like this:

// constructor(x,y){this.x = x; this.y = y; }toString() {return (this.x + "The age is." +this.y+"Age"); }}export {Person};

//index.js
import {Person} from './Person';
let person = new Person('Joe', 12); console.log(person.toString()); / Zhang SAN's age is 12Copy the code

The face code defines a “Class Class”, which contains a constructor method, and the this keyword represents the instance object. That is, the Person constructor of ES5 corresponds to the constructor of the Person class of ES6. In addition to the constructor, the Person class defines a toString method.

When you define a method of a class, you don’t need to use the function keyword; you can just use the function definition. In addition, methods do not need to be separated by commas, which will cause errors.

A class must have a constructor method; if not explicitly defined, a default constructor method is added. So even if you don’t add a constructor, there’s a default constructor.

2. Document fragments in JS

In browsers, we usually insert a DOM node into the page using innerHTML() or appendChild(), as in:

for(var i=0; i<5; i++){ var op = document.createElement("span"); 

    var oText = document.createTextNode(i); 

    op.appendChild(oText); 

    document.body.appendChild(op); 
}
Copy the code

However, if we want to add a large amount of data to the document (say 1W), the process can be slow if we add nodes one by one as in the code above. Of course, you could create a new node, such as div, adding oP to div and then div to body, but adding another

to body. Document fragmentation does not produce such nodes.

var oDiv = document.createElement("div"); 

for(var i=0; i<10000; i++){ var op = document.createElement("span"); 

    var oText = document.createTextNode(i); 

    op.appendChild(oText); 

    oDiv.appendChild(op);  
} 
document.body.appendChild(oDiv);
Copy the code

To solve this problem, JS introduces the createDocumentFragment() method, which creates a document fragment, appends the new node to it, and then adds it to the document at once. The code is as follows:

/ / to create a document fragment var oFragmeng = document. CreateDocumentFragment ();for(var i=0; i<10000; i++){ var op = document.createElement("span"); var oText = document.createTextNode(i); op.appendChild(oText); Ofragmeng.appendchild (op); } / / finally disposable added to the document in the document. The body. The appendChild (oFragmeng);Copy the code

3. Deconstruct assignments

The process of assigning values from arrays or objects in a certain pattern is called deconstruction.

4. Array. The from and Array. Reduce

1. Array.from() in ES6

Two classes of objects are used to turn them into true arrays: array-like objects and iterable objects (including the new data structures Set and Map in ES6).

Here is an array-like object, array. from converts it to a real Array:

let arrayLike = {
    '0': 'a'.'1': 'b'.'2': 'c',
    length: 3
};
Copy the code
Var arr1 = [].slice.call(arrayLike); / / /'a'.'b'.'c']
Copy the code
// ES6letarr2 = Array.from(arrayLike); / / /'a'.'b'.'c']
Copy the code

In practice, common array-like objects are the NodeList collection returned by DOM operations, and arguments objects inside functions. Array.from can turn them into real arrays.

/ NodeList objectlet ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
  returnp.textContent.length > 100; }); / / the arguments objectfunction foo() {
  var args = Array.from(arguments);
  // ...
}
Copy the code

In the above code, the querySelectorAll method returns an array-like object that can be converted to a real array using the Filter method.

More reference

2. Array.reduce() method in ES5

The reduce() method receives a function as an accumulator, and each value in the array (from left to right) is merged to a single value.

parameter describe
callback A function that executes each value in the array and contains four arguments
previousValue The value returned from the last call to the callback, or the initialValue provided
currentValue The element in the array currently being processed
index The index of the current element in the array
array An array of calls to reduce
initialValue As the first argument to the first callback call.

A detailed description

  • Reduce executes the callback function for each element in the array, excluding elements that were deleted or never assigned, and takes four arguments: the initial value (or the value returned by the previous callback), the current element value, the current index, and the array that called Reduce.

  • When the callback function is first executed, previousValue and currentValue can be the same value. If initialValue is provided when reducing is called, then the first previousValue equals initialValue, And currentValue is equal to the first value in the array; If initialValue is not provided, previousValue is equal to the first value in the array, and currentValue is equal to the second value in the array.

  • If the array is empty and no initialValue is provided, TypeError is raised. If the array has only one element (regardless of position) and no initialValue is provided, or if initialValue is provided but the array is empty, this unique value will be returned and the callback will not be executed.

Example:

var total = [0, 1, 2, 3].reduce(function(a, b) {
    returna + b; }); console.log(total); //6 var total = [0, 1, 2, 3].reduce(function(a, b) {
    returna + b; }, 10); console.log(total); / / 16Copy the code

5. Use recursion

6. Obj. Keys () and Obj. DefineProperty ()

Obj.keys(

The object.keys () method returns an array of the self-enumerable properties of a given Object.

Example:

var person = {
       firstName: "aaaaaa",
       lastName: "bbbbbb",
       others: "ccccc"
};
Object.keys(person).forEach(function(data) {
       console.log('person', data, ':', person[data]);
 });
//console.log:
//person firstName : aaaaaa
//person lastName : bbbbbb
//person others : ccccc
Copy the code

The use of obj.defineProperty ()

The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.

Grammar:

Object.defineProperty(obj, prop, descriptor)
Copy the code

Parameter Description:

Obj: Absolutely. Target object prop: Required. Attribute name descriptor to be defined or modified: required. Properties owned by the target propertyCopy the code

The return value:

The object passed to the function. That's the first parameter objCopy the code

Adding a property description to an object’s properties is currently available in two forms: data description and accessor description.

I. Data description

When modifying or defining a property of an object, add some properties to the property:

var obj = {
    test:"hello"Object.defineproperty (obj,"test",{
    configurable:true | false,
    enumerable:true | false, value: any type of value, writable:true | false}); Object.defineproperty (obj,"newKey",{
    configurable:true | false,
    enumerable:true | false, value: any type of value, writable:true | false
});
Copy the code

Summary of features set:

Value: sets the value of an attribute. Writable: indicates whether the value can be rewritten.true | falseEnumerable: Whether a target property can be enumerable.true | falseThe target property can be deleted or modified againtrue | false
Copy the code

Note:

  • In addition to newly defined properties, you can set properties for existing properties

  • DefineProperty allows you to add any property to the Object, and the default values of Enumerable, writable, and more are false if the property is not specified.

Accessor description

When using accessors to describe properties, it is possible to set the following properties:

var obj = {};
Object.defineProperty(obj,"newKey",{
    get:function (){} | undefined,
    set:function (value){} | undefined
    configurable: true | false
    enumerable: true | false
});
Copy the code

Note: When using getter or setter methods, the writable and value properties are not allowed

Getter /setter When setting or obtaining the value of a property of an object, you can provide getter/setter methods.

  • A getter is a way of getting the value of an attribute

  • A setter is a method of setting the value of a property. Use the GET /set attribute in the attribute to define the corresponding method.

var obj = {};
var initValue = 'hello';
Object.defineProperty(obj,"newKey",{
    get:function(){// The function that fires when a value is fetchedreturn initValue;    
    },
    set:function(value){// This function is triggered when a value is set. }}); // Get the value console.log(obj.newKey); //hello // set the value obj.newKey ='change value';
console.log( obj.newKey ); //change value
Copy the code

Note: Get or set doesn’t have to be a pair, just one or the other. If no method is set, the default values of get and set are undefined. Different and Enumerable are the same as above.

7. Publish and subscribe

Issue 1: Publish – subscribe, also known as the observer model

Observer model conceptual interpretation

The observer mode is also called Publish/Subscribe mode. It defines a one-to-many relationship, allowing multiple observer objects to listen to a topic object at the same time. When the status of the topic object changes, all observer objects will be notified, so that they can automatically update themselves.

Observer mode functions and considerations

Mode role:

  • 1, support simple broadcast communication, automatic notification of all subscribed objects.

  • 2. After the page is loaded, the target object is easily associated with the observer dynamically, which increases flexibility

  • The abstract coupling between the target object and the observer can be independently extended and reused.

Matters needing attention:

Listen before you trigger.

So far, the principle of two-way data binding and the idea of implementation has been basically completed, but also explained the basic knowledge needed in the implementation of the function, hope readers can gain knowledge from it, but also welcome to put forward different opinions, humbly adopt ~~~