March and April are the recruitment season, I believe many students have been asked what is the principle of VUE? This article shows you how to implement the basic functionality of the VUE framework in the simplest possible way. In order to reduce your learning cost, I will teach you a VUE framework in the simplest way.

A, preparation,

I hope you have the following skills as you prepare to read this article:

  • Familiar with ES6 grammar
  • Understand HTML DOM node types
  • Be familiar withObject.defineProperty()Use of methods
  • Basic use of regular expressions. (e.g. Grouping)

First, let’s create an HTML file by following the code below. This article will show you how to do this.

   <script src=".. /src/vue.js"></script>
</head>
<body>
    <div id="app">
        <! -- analytic interpolation -->
        <h2>The title is {{title}}</h2>
        <! -- Parse common commands -->
        <p v-html='msg1' title='Confounding attribute 1'>Confusing text 1</p>
        <p v-text='msg2' title='Confounding attribute 2'>Confusing text 2</p>
        <input type="text" v-model="something">
        <! -- Two-way data binding -->
        <p>{{something}}</p>
        <! -- Complex data types -->
        <p>{{dad.son.name}}</p>
        <p v-html='dad.son.name'></p>
        <input type="text" v-model="dad.son.name"> 
        
        <button v-on:click='sayHi'>sayHi</button>
        <button @click='printThis'>printThis</button>
    </div>
</body>
Copy the code
 let vm = new Vue({
        el: '#app'.data: {
            title: 'Hand in hand teach you a vue frame'.msg1: ' should parse into a tag '.msg2: ' should not parse into a tag '.something: 'placeholder'.dad: {
                name: 'foo'.son: {
                    name: 'bar'.son: {}}}},methods: {
            sayHi() {
                console.log('hello world')
            },
            printThis() {
                console.log(this)}}})Copy the code

With the preparation done, let’s implement the basic functions of the VUE framework together!

MVVM implementation

As we all know, VUE is a progressive framework based on the MVVM design pattern. So how do we implement an MVVM framework in JavaScript? There are three main ways to implement the MVVM framework:

  • backbone.js

The publish-subscriber mode, which is generally implemented by means of pub and sub, binds data and views.

  • Angular.js

Angular.js uses dirty value monitoring to determine whether to update the view by comparing data changes. Similar to using the timer wheel to monitor whether the data has changed.

  • Vue.js

Vue.js uses data hijacking combined with the publisher-subscriber model. Prior to VVUe2.6, the setter and getter methods for each property were hijacked via object.defineProperty (), Posting messages to subscribers when the data changed, triggering the corresponding callback. This is also the root reason why vUE is not supported in browsers below IE8.

Vue implementation idea

  • Implement a Compile template parser that can parse instructions and interpolation expressions in the template and assign corresponding operations
  • Implement an Observer data listener that listens for all properties of a data object
  • Implement a Watcher listener. The parse results of Compile, connect with the objects observed by Observer, establish relationships, receive notifications when the data object changes observed by Observer, and update the DOM
  • Create a common entry object (Vue), receive the initial configuration, and coordinate the Compile, Observer, and Watcher modules, i.e. Vue.

The above process is shown in the figure below:

Vue entry file

Once the logic is in order, we can see that what we want to do in this entry file is very simple:

  • Mount data and methods to the root instance;
  • Use the Observer module to listen for changes to all attributes of data
  • If there is a mount point, all instructions and interpolations under that mount point are compiled using the Compile module
/** * vue.js (entry file) * 1. Mount the attributes in data,methods to the root instance * 2. Listen for changes to the data attribute * 3. Compile all instructions and interpolation */ within the mount point
class Vue {
    constructor(options={}){
        this.$el = options.el;
        this.$data = options.data;
        this.$methods = options.methods;
        debugger
        // Mount the attributes from data and methods to the root instance
        this.proxy(this.$data);
        this.proxy(this.$methods);
        // Listen for data
        // new Observer(this.$data)
        if(this.$el) {
        // new Compile(this.$el,this);
        }
    }
    proxy(data={}){
        Object.keys(data).forEach(key= >{
            // This refers to the vue instance
            Object.defineProperty(this,key,{
                enumerable: true.configurable: true,
                set(value){
                    if(data[key] === value) return
                    return value
                },
                get(){
                    return data[key]
                },
            })
        })
    }
}
Copy the code

Compile module

Main do is to parse the compile command (attribute node) and the interpolation expressions (text nodes), will be replaced with data in the template variables, and then initializes rendering page view, and the corresponding node binding update each instruction function, add the monitor data of subscribers, received notice, once the data has changed to update the view.

Because walking through the parsing process involves multiple dom nodes, which can lead to backflow and redrawing of the page, it is better to parse instructions and interpolations in memory for better performance and efficiency, so we need to walk through everything under the mount point and store it in DocumentFragments.

DocumentFragments are DOM nodes. They are not part of the main DOM tree. The usual use case is to create a document fragment, attach elements to the document fragment, and then attach the document fragment to the DOM tree. Because the document fragment exists in memory and not in the DOM tree, inserting child elements into the document fragment does not cause page backflow (a calculation of element location and geometry). Therefore, using document fragments generally results in better performance.

So we need a node2Fragment () method to handle the above logic.

Implement node2Fragment to store all nodes within the mount point into the DocumentFragment

node2fragment(node) {
    let fragment = document.createDocumentFragment()
    // Add all the child nodes in the EL to the document fragment one by one
    let childNodes = node.childNodes
    // Since childNodes is an array, we will convert it to an array to use the forEach method
    this.toArray(childNodes).forEach(node= > {
        // Add all byte points to the fragment
        fragment.appendChild(node)
    })
    return fragment
}
Copy the code

This.toarray () is a class method THAT I encapsulate to convert a class array to an array. The implementation method is also simple, I used the most common development techniques:

toArray(classArray) {
    return [].slice.call(classArray)
}
Copy the code

Parse nodes in fragments

The next thing we need to do is parse the node in the fragment: compile(fragment).

The logic of this method is simple. We recursively iterate through all the child nodes in the fragment, judging by the type of node, parsing text nodes by interpolation, and property nodes by instruction. When parsing the attribute node, we need to further determine whether it is an instruction beginning with v- or a special character, such as @, :.

// Compile.js
class Compile {
    constructor(el, vm) {
        this.el = typeof el === "string" ? document.querySelector(el) : el
        this.vm = vm
        // Parse the template content
        if (this.el) {
        // To avoid backflow and redrawing caused by parsing instructions and difference expressions directly in the DOM, we create a Fragment to parse them in memory
        const fragment = this.node2fragment(this.el)
        this.compile(fragment)
        this.el.appendChild(fragment)
        }
    }
    // Parse the node in the fragment
    compile(fragment) {
        let childNodes = fragment.childNodes
        this.toArray(childNodes).forEach(node= > {
            // Parse the instruction if it is an element node
            if (this.isElementNode(node)) {
                this.compileElementNode(node)
            }

            // Parse the difference expression if it is a text node
            if (this.isTextNode(node)) {
                this.compileTextNode(node)
            }

            // Recursive parsing
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }
        })
    }
}
Copy the code

The logic that handles parsing instructions: CompileUtils

All we need to do now is parse the instruction and inform the view of the result.

When the data changes, the Watcher object listens for changes in the EXPr data, and executes the callback function once the data changes.

New Watcher(VM,expr,callback) // Uses Watcher to return the parsed result to the view.

We can encapsulate all the logic that handles compilation instructions and interpolations into compileUtil objects for management.

Here are two pits you need to pay attention to:

  1. In the case of complex data, such as interpolation:{{dad.son.name}}or<p v-text='dad.son.name'></p>We’ve gotv-textThe value of the property is a stringdad.son.nameWe can’t get throughvm.$data['dad.son.name']To get the data, it’s to get throughvm.$data['dad']['son']['name']To get the data. Therefore, if the data is complex data case, we need to implementgetVMData()andsetVMData()Method to obtain and modify data.
  2. In vue, the “this” in the methods is referring to the vue instancev-onWhen a directive binds a method to a node, we need to bind the method’s this point to the vue instance.
// Compile.js
let CompileUtils = {
    getVMData(vm, expr) {
        let data = vm.$data
        expr.split('. ').forEach(key= > {
            data = data[key]
        })
        return data
    },
    setVMData(vm, expr,value) {
        let data = vm.$data
        let arr = expr.split('. ')
        arr.forEach((key,index) = > {
            if(index < arr.length - 1) {
                data = data[key]
            } else {
                data[key] = value
            }
        })
    },
    // Parsed interpolation
    mustache(node, vm) {
        let txt = node.textContent
        let reg = / \ {\ {(. +) \} \} /
        if (reg.test(txt)) {
            let expr = RegExp. $1
            node.textContent = txt.replace(reg, this.getVMData(vm, expr))
            new Watcher(vm, expr, newValue => {
                node.textContent = txt.replace(reg, newValue)
            })
        }
    },
    / / parsing v - text
    text(node, vm, expr) {
        node.textContent = this.getVMData(vm, expr)
        new Watcher(vm, expr, newValue => {
            node.textContent = newValue
        })
    },
    / / parsing v - HTML
    html(node, vm, expr) {
        node.innerHTML = this.getVMData(vm, expr)
        new Watcher(vm, expr, newValue => {
            node.innerHTML = newValue
        })
    },
    / / parsing v - model
    model(node, vm, expr) {
        let that = this
        node.value = this.getVMData(vm, expr)
        node.addEventListener('input'.function () {
            // The following notation does not change the data in depth
            // vm.$data[expr] = this.value
            that.setVMData(vm,expr,this.value)
        })
        new Watcher(vm, expr, newValue => {
            node.value = newValue
        })
    },
    / / parsing v - on
    eventHandler(node, vm, eventType, expr) {
        (fn does not exist)
        // If fn is not written, it will not affect the project to continue running
        let fn = vm.$methods && vm.$methods[expr]
        
        try {
            node.addEventListener(eventType, fn.bind(vm))
        } catch (error) {
            console.error('Throwing this exception means you don't have a method \n in your methods.', error)
        }
    }
}
Copy the code

Observer module

In fact, in the Observer module, we don’t have much to do but provide a walk() method that recursively hijacks all data in vm.$data and intercepts the setters and getters. If the data changes, a notification is published, allowing all subscribers to update the content and change the view.

Note that if the value is set to an object, we need to ensure that the object is also responsive. Walk (aObjectValue). Our approach to implementing reactive objects is object.defineProperty ()

The complete code is as follows:

// Observer.js
class Observer { 
    constructor(data){
        this.data = data
        this.walk(data)
    }
    
    // Iterate through all the data in the walk, hijacking the set and get methods
    walk(data) {
        // Determine if data does not exist or is not an object
        if(! data ||typeofdata ! = ='object') return

        // Retrieve all attributes from data
        Object.keys(data).forEach(key= > {
            // console.log(key)
            // Add getters and setters to properties in data
            this.defineReactive(data,key,data[key])

            // If data[key] is an object, depth hijacking
            this.walk(data[key])
        })
    }

    // Define reactive data
    defineReactive(obj,key,value) {
        let that = this
        // The Dep message container is declared in the watcher.js file. Commenting out observer.js code related to the Dep container does not affect its logic.
        let dep = new Dep()
        Object.defineProperty(obj,key,{
            enumerable:true.configurable: true,
            get(){
                // If there is a watcher object in the dep. target, it is stored in the subscriber array
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set(aValue){
                if(value === aValue) return
                value = aValue
                // If the value set is an object, then the object should also be responsive
                that.walk(aValue)

                // watcher.update
                // Publish notifications for all subscribers to update the content
                dep.notify()
            }
        })
    }
} 
Copy the code

5. Watcher module

Watcher’s function is to establish a relationship between the results of Compile parsing and the objects observed by observers. When the data observed by observers changes, a notification (dep.notify) tells Watcher that Watcher is updating the DOM with Compile. This involves the idea of a publisher-subscriber model.

Watcher is the bridge between Compile and Observer.

In the Watcher constructor, we need to pass three arguments:

  • vmExample: the vue
  • exprName of data in vm.$data (key)
  • callback: Callback function that is executed when data changes

Note that in order to get the deep data object, here we need to refer to the previously declared getVMData() method.

Define the Watcher

constructor(vm,expr,callback){
    this.vm = vm
    this.expr = expr 
    this.callback = callback
    
    //
    this.oldValue = this.getVMData(vm,expr)
    //
}
Copy the code

Expose the update() method, which is used to update the page when data is updated

When should we update the page?

We should implement an update method in Watcher to compare the new value with the old value. When the data changes, the callback function is executed.

update() {
    // Compare whether expr has changed, and call callback if it has
    let oldValue = this.oldValue
    let newValue = this.getVMData(this.vm,this.expr)

    // Callback is called when a change is made
    if(oldValue ! == newValue) {this.callback(newValue,oldValue)
    }
}
Copy the code

Associate Watcher with Compile

vm.msg

// compile.js
mustache(node, vm) {
    let txt = node.textContent
    let reg = / \ {\ {(. +) \} \} /
    if (reg.test(txt)) {
        let expr = RegExp. $1
         node.textContent = txt.replace(reg, this.getVMData(vm, expr))
         
         // Listen for changes in the expr value. When the value of expr changes, the callback function is executed
        new Watcher(vm, expr, newValue => {
            node.textContent = txt.replace(reg, newValue)
        })
    }
},
Copy the code

So when should we call the update method and trigger the callback function?

Since we have implemented responsive data in the Observer above, the set method must be triggered when the data changes. So while we’re firing the set method, we also need to call the watcher.update method, fire the callback function, and modify the page.

// observer.js
defineReactive(obj,key,value) {
    ...
    set(aValue){
        if(value === aValue) return
        value = aValue
        // If the value set is an object, then the object should also be responsive
        that.walk(aValue)

        watcher.update
    }
}
Copy the code

Which Watcher’s update method should be called? Which Watcher’s update method should be called? How do you notify all Watcher that the data has changed?

So here comes a new concept: the publisher-subscriber model.

What is the publisher-subscriber model?

The publisher-subscriber pattern is also called the observer pattern. He defined a one-to-many dependency relationship, that is, when an object’s state changes, all objects that depend on it are notified and automatically updated, solving the coupling of functions between the subject object and the observer.

Here we use wechat public account as an example to illustrate this situation.

For example, if a class has subscribed to a public account, everyone in the class is a subscriber, and the public account is a publisher. If one day the official account found an error in the content of the article and needed to fix a typo (modify the data in vm.$data), is it necessary to notify each subscriber? The article that cannot study committee there has changed, and monitor’s article has not changed. In this process, the publisher doesn’t have to care who subscribed to it; it just needs to send the update to all subscribers (notify).

So there are two processes involved:

  • Add a subscriber:addSub(watcher)
  • Push notifications:notify(){ sub.update() }

In this process, the role of the publisher is an object that each subscriber depends on.

We define a class in Watcher: Dep (dependent container). Every time we add a new Watcher, we add subscribers to the Dep. If the Observer data changes, notify the Dep and update the DOM.

// watcher.js
// The subscriber container, dependent collection
class Dep {
    constructor() {// Initialize an empty array to store subscribers
        this.subs = []
    }

    // Add subscribers
    addSub(watcher){
        this.subs.push(watcher)
    }
 
    / / notice
    notify() {
        // Notify all subscribers of the page change
        this.subs.forEach(sub= > {
            sub.update()
        })
    }
    
}
Copy the code

The next step is to store a new Watcher in the Dep container each time. Associate Dep with Watcher. We can add a class attribute, target, to Dep to store the Watcher object. We need to assign this to dep. target in the constructor of Watcher.

  1. First we’re going to go into the Observer and hijack the data MSG in the data, and here we’re going to go into the get method in the Observer;
  2. After hijacking, we will determine whether el exists, and Compile the interpolation into Compile;
  3. If the hijacked MSG changes, the Watcher in Mustache listens for the change;
  4. In Watcher’s constructor, passthis.oldValue = this.getVMData(vm, expr)The get method enters the Observer at once, and the program is finished.

Therefore, it is not difficult to find the time to add subscribers. The code is as follows:

  • Add Watcher to the subscriber array and initiate notification for all subscribers if the data changes
// Observer.js
// Define reactive data
defineReactive(obj,key,value) {
    // defineProperty changes this orientation
    let that = this
    let dep = new Dep()
    Object.defineProperty(obj,key,{
        enumerable:true.configurable: true,
        get(){
            // If dep. target exists, then the watcher object is stored in the subscriber array
            // debugger
            Dep.target && dep.addSub(Dep.target)
            return value
        },
        set(aValue){
            if(value === aValue) return
            value = aValue
            // If the value set is an object, then the object should also be responsive
            that.walk(aValue)

            // watcher.update
            // Publish notifications for all subscribers to update the content
            dep.notify()
        }
    })
}
Copy the code
  • After you store the Watcher in the Dep container, set dep. target to empty to store the Watcher the next time
// Watcher.js
constructor(vm,expr,callback){
    this.vm = vm
    this.expr = expr 
    this.callback = callback

    Dep.target = this
    // debugger
    this.oldValue = this.getVMData(vm,expr)

    Dep.target = null
}
Copy the code

The complete code for watcher.js is as follows:

// Watcher.js

class Watcher {
    /** ** @param {*} Current vUE instance of the VM * @param {*} Expr data name * @param {*} callback If data changes, callback */ needs to be called
    constructor(vm,expr,callback){
        this.vm = vm
        this.expr = expr 
        this.callback = callback

        Dep.target = this

        this.oldValue = this.getVMData(vm,expr)

        Dep.target = null
    }

    // The exposed method used to update the page
    update() {
        // Compare whether expr has changed, and call callback if it has
        let oldValue = this.oldValue
        let newValue = this.getVMData(this.vm,this.expr)

        // Callback is called when a change is made
        if(oldValue ! == newValue) {this.callback(newValue,oldValue)
        }
    }

    // For the sake of the principle, I will not extract the public JS file
    getVMData(vm,expr) {
        let data = vm.$data
        expr.split('. ').forEach(key= > {
            data = data[key]
        })
        return data
    }
}

class Dep {
    constructor() {this.subs = []
    }

    // Add subscribers
    addSub(watcher){
        this.subs.push(watcher)
    }
 
    / / notice
    notify() {
        this.subs.forEach(sub= > {
            sub.update()
        })
    }
    
}
Copy the code

At this point, we have implemented the basic functionality of the Vue framework.

This article will simulate the basic functionality of the VUE framework in the simplest possible way, so you’ll have to forgive me for sacrificing a lot of detail and code quality.

It is inevitable that the article will have some not rigorous place, welcome everyone to correct, interested words we can communicate together