If you have learned VUE, how can you say you are proficient in using VUE if you do not understand the principle of responsiveness? How can you say you are proficient in using VUE if you have not written a simple version of VUE? This article takes you to write a simple version of VUE through more than 300 lines of code. The main realization of VUE data response type (data hijacking combined with publisher – subscriber), array variation method, compilation instructions, data binding functions.

This article requires a certain vUE foundation, not suitable for beginners to learn. Vue learning link is attached at the end of the article. Because this article uses a lot of knowledge, in the end of the article also has the relevant knowledge links and VUE source address, you go to the end of the article to see oh ~

This article is a long and difficult one, so find a quiet place, shower and dress before reading, and keep your programming sacred. Below is the implementation of a simple version of vue source address, be sure to download down first! Because not all of the code is in the article.

Github source address: github.com/ucasey/myvu…

Before we get started, let’s look at what MVVM is and what data responsiveness is.

We all know that VUE is a typical MVVM idea, driven by data views.

So what is the MVVM idea?

MVVM is model-view-viewModel, which divides a system into Model, View and view-Model.

Vue in MVVM, there is no direct connection between view and model, but the view and view-Model, model and view-Model interaction, when the VIEW view dom operations to change the data, The view-Model can be synchronized to the Model, and the same changes to the Model data will be synchronized to the View.

So what are the methods for implementing data responsiveness?

The publiser-subscriber pattern: When an object (publisher) changes its state, all dependent objects (subscribers) are notified. In layman’s terms, publishers are newspapers and subscribers are people who read newspapers.

2. Dirty check: Determine whether to update the view by storing the old data and comparing it with the current new data to see if there is any change. Angular. js is the way to pass dirty value checking. The simplest way to do this is to detect data changes with a scheduled setInterval() poll, but this definitely increases performance, so Angular only enters dirty value detection when a specified event is triggered.

3. Data hijacking: Use Object.defineProperty() to hijack the setter and getter of each property to trigger the corresponding method when the data changes.

How does VUE implement data responsiveness?

Vue.js combines data hijacking with the publisher-subscriber pattern.

Image from Internet

When new Vue() is executed, the Vue enters the initialization phase, and the Vue parses the instructions (initialize the view, add subscribers, bind the update function), Obserber iterates through the data and listens for the getter and setter of Object.defineProperty. When the data changes, setter methods in the Observer are fired. The setter immediately calls dep.notify (), and the Dep starts iterating through all the subscribers and calls the subscriber’s update method, which updates the view accordingly when the subscriber is notified.

Let me introduce the important nouns in turn:

Observer: a data listener that monitors all attributes of a data Object and notifies subscribers of any changes to the latest values. This is implemented internally using getters and setters for Object.defineProperty

Compile: An instruction parser, which scans and parses the instructions of each element node, replaces the data according to the instruction template, and binds the corresponding update function

Dep: Subscriber collector, or message subscriber, either maintains an array internally that collects subscribers and calls the update method of the subscribers when the notify function is triggered by data changes

Watcher: The subscriber, which acts as a bridge between the Observer and Compile, receives notification from the message subscriber to update the view

Updater: View update

So we want to implement a VUE responsive, data hijacking, dependency collection, publisher subscriber pattern.

Here I will introduce my imitation of the source code implementation of the function:

  1. Reactive, bidirectional binding of data, which can listen for all attributes of the data object, get the latest value and notify the subscriber if there is any change
  2. Parse vUE directives v-html, V-text, V-bind, V-ON, V-model, including (@:
  3. Array variation method processing
  4. Use this in Vue to access or change the data in data

To do this, we need to implement the following classes and methods:

  1. Implement an Observer class that listens for all data
  2. Implement array tool method: the processing of the mutation method
  3. Implement Dep classes: Maintain subscribers
  4. Implement the Watcher class to receive update notifications from the Dep for updating the view
  5. Compile class: used to parse instructions
  6. Implement compileUtils tool methods that update views with instructions and bind to the update function Watcher
  7. Implement this. Data proxy: Implement this. Data proxy: Implement this. Data proxy, you can directly use this in vue to get the current data

I use Webpack as a building tool for collaborative development, so I will use ES6 modular, webpack related knowledge in my vUE response. The knowledge link is at the end of the article.

1. Implement the Observer class

We all know that to listen for property data changes with obeject.defineProperty (), we need to recursively traverse the Observer data objects, including the attributes of the child property objects, with setters and getters, When you assign a value to this object, it fires the setter, so you can listen for changes in the data. Of course, as we add new data, we’re going to recursively iterate over the new data object, adding setters and getters.

But when you’re dealing with an array, you’re not adding a setter and getter for every element in the array. If you think about it, an array of data that comes back from the back end is huge. If you add a setter and getter for every property, the performance cost is huge. The effect we want to get is not proportional to the performance consumed, so in the aspect of the array, we realize the data response through the array of 7 variation methods. The page is rerendered only if the array is modified or deleted using the array mutator method.

So how do you notify the subscriber to update the view once changes are heard? We need to implement a Dep (message subscriber) with a notify() method that notifies the subscriber that the data has changed and then asks the subscriber to update the view.

How do we add subscribers? We can add subscribers via new Dep(), through the addSaubs() method in Dep. Let’s look at the code.

We first need to declare an Observer class. When we create the class, we need to create a message subscriber and determine if it is an array. If it is an array, we modify the array. If it is an object, we need to add a setter and getter for each property of the object.

import { arrayMethods } from './array' // Array mutation method processing
class Observer {
  constructor(data) {
    // It is used to process the array and store the observer of the array
    this.dep = new Dep()
    if (Array.isArray(data)) {
      // If it is an array, use the array variation method
      data.__proto__ = arrayMethods
      // Add an __ob__ Observer to the array data. When using the array variation method, the view can be updated
      data.__ob__ = this
      // Add data hijacking to each item of the array (setter/getter handling)
      this.observerArray(data)
    } else {
      // Add data hijacking for non-array data (setter/getter handling)
      this.walk(data)
    }
  }
}
Copy the code

Above, we reassign data’s __proto__ prototype chain. Let’s see what arrayMethods is. ArrayMethods is a new array prototype thrown in array.js

// Get the Array prototype chain
const arrayProto = Array.prototype;
// Create a new object with the corresponding prototype, called a new Array below
const arrayMethods = Object.create(arrayProto);
// Handle 7 array variation methods
['push'.'pop'.'shift'.'unshift'.'reverse'.'sort'.'splice'].forEach(ele= > {
    // Modify the corresponding method of the new Array
    arrayMethods[ele] = function () {
        // Executes the array's native method to do what it needs to do
        arrayProto[ele].call(this. arguments)// Get the Observer object
        const ob = this.__ob__
        // Update the view
        ob.dep.notify()
    }
})
export {
    arrayMethods
}
Copy the code

Now, we have a mutating method for the array, and we also need to add getters and setters for each item of the array using the observerArray method, and notice that each item is just the outermost layer, not a recursive traversal.

// Loop through the array, setting the setter/getter for each entry in the array
observerArray(items) {
    for (let i = 0; i < items.length; i++) {
      this.observer(items[i])
    }
}
Copy the code

If it’s an object, we iterate over each property of the object recursively, using the walk() method

walk(data) {
	// Data hijacking
    if (data && typeof data === "object") {
      for (const key in data) {
        // Bind the setter and getter
        this.defineReactive(data, key, data[key])
      }
    }
}
Copy the code

DefineReactive () is called above. What does this method do? This method is set to data hijacking, with comments on each line.

// Data hijacking, set setter/getteer
  defineReactive(data, key, value) {
    // If it is an array, you need to accept the returned Observer object
    let arrayOb = this.observer(value)
    // Create subscribers/collect dependencies
    const dep = new Dep()
    // Setter and getter handling
    Object.defineProperty(data, key, {
      // enumerable
      enumerable: true.// modifiable
      configurable: false.get() {
        // Add watcher when Dep has watcher
        Dep.target && dep.addSubs(Dep.target)
        // If it is an array, add the observer of the array
        Dep.target && arrayOb && arrayOb.dep.addSubs(Dep.target)
        return value
      },
      set: (newVal) = > {
        // Change if new and old data are not equal
        if(value ! == newVal) {// Add a setter/getter for the newly set data
          arrayOb = this.observer(newVal);
          value = newVal
          // Notify the DEP that the data has changed
          dep.notify()
        }
      }
    })
  }
}
Copy the code

It is important to note that in the diagram above, in the Observer, the message subscriber is notified if data changes. When should the message subscriber be bound? Dep. Target && Dep.addSubs (dep.target), which adds a subscriber to Dep by calling the addSubs method of Dep

2. Implement Dep

Dep is a message subscriber. It maintains an array of subscribers and notifies the corresponding subscribers when the data is sent. There is a notify() method in Dep that notifies the subscribers when the data is sent

// Subscriber collector
export default class Dep {
    constructor() {
        // Manage the watcher array
        this.subs = []
    }
    addSubs(watcher) {
        / / add a watcher
        this.subs.push(watcher)
    }
    notify() {
        // Tell Watcher to update the DOM
        this.subs.forEach(w= > w.update())
    }
}
Copy the code

Implement Watcher

Watcher is the subscriber. Watcher is the communication bridge between the Observer and Compile. When data changes, Watcher receives notification from Dep (notify() of Dep), calls its update() method, and triggers the callback bound in Compile. Achieve the purpose of updating the view.

import Dep from './dep'
import { complieUtils } from './utils'
export default class Watcher {
    constructor(vm, expr, cb) {
        // The current vue instance
        this.vm = vm;
        / / expression
        this.expr = expr;
        // The dom is updated
        this.cb = cb
        // Get the old data. Dep.target will bind the current this when getting the old value
        this.oldVal = this.getOldVal()
    }
    getOldVal() {
        // Bind the current watcher
        Dep.target = this
        // Get old data
        const oldVal = complieUtils.getValue(this.expr, this.vm)
        // After the binding is complete, the binding is empty to prevent multiple binding
        Dep.target = null
        return oldVal
    }
    update() {
        // Update function
        const newVal = complieUtils.getValue(this.expr, this.vm)
        if(newVal ! = =this.oldVal || Array.isArray(newVal)) {
            // A callback function passed in when watcher was created with an update in compile
            this.cb(newVal)
        }
    }
}
Copy the code

The getValue() method of complieUtils, described below, is used to get the value of the specified expression.

If we divide the whole process into two lines:

New Vue() ==> Observer data hijack ==> Bind Dep ==> notify Watcher ==> update the view

New Vue() ==> Compile parse template directives ==> initialize views and bind watcher

Now that we’ve implemented the first line, let’s implement the second line.

4. Compile

Compile mostly parses template instructions, replaces variables in the template with data, and initializes the render page view. Also bind the update function to add subscribers.

In the process of parsing, DOM will be manipulated for many times. In order to improve performance and efficiency, THE EL of the root node of vUE instance will be converted into document fragment for parsing and compilation. After parsing, the fragment will be added back to the original real DOM node.

class Complie {
    constructor(el, vm) {
        this.el = this.isNodeElement(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 1. Place all DOM objects into fragement to prevent repeated dom manipulation and performance degradation
        const fragments = this.nodeTofragments(this.el)
        // create a template
        this.complie(fragments)
        // append the child element to the root element
        this.el.appendChild(fragments)
    }  
}
Copy the code

The first step is to use nodeTofragments to add all DOM nodes to the document fragment. The last step is to readd parsed DOM elements from the document fragment to the page. Download the source code for these two steps. If you look at it, it’s annotated. I won’t explain it again.

Let’s look at step 2, compiling templates:

 complie(fragments) {
    // Get all nodes
    const nodes = fragments.childNodes;
    [...nodes].forEach(ele= > {
        if (this.isNodeElement(ele)) {
            //1. Compile the element node
            this.complieElement(ele)
        } else {
            // Compiles the text node
            this.complieText(ele)
        }
        // If there are child nodes, loop over and compile the instruction
        if (ele.childNodes && ele.childNodes.length) {
            this.complie(ele)
        }
    })
}
Copy the code

It is important to remember that templates can have two types: text nodes (interpolation expressions with double braces) and element nodes (instructions). After we get all the nodes, we judge each node. If it is an element node, we use the method that parses the element node. If it is a text node, we call the method that parses the text.

complieElement(node) {
    //1. Get all attributes
    const attrs = node.attributes;
    //2. Filter is attribute
    [...attrs].forEach(attr= > {
        //attr is an object,name is the attribute name,value is the attribute value
        const {name,value} = attr
        // Check whether it starts with v- such as v-html
        if (name.startsWith("v-")) {
            // Separate directives text, HTML, on:click
            const [, directive] = name.split("-")
            // Handle on:click or bind:name cases on,click
            const [dirName, paramName] = directive.split(":") 
            // Compile the template
            complieUtils[dirName](node, value, this.vm, paramName)
            // Remove attributes. The attributes of the V-html directive are no longer displayed in the DOM of the page
            node.removeAttribute(name)
        } else if (name.startsWith("@")) {
            @click='handleClick'
            let [, paramName] = name.split(The '@');
            complieUtils['on'](node, value, this.vm, paramName);
            node.removeAttribute(name);
        } else if (name.startsWith(":")) {
            Href ='... '
            let [, paramName] = name.split(':');
            complieUtils['bind'](node, value, this.vm, paramName); node.removeAttribute(name); }})}Copy the code

We call the complieUtils[dirName](node, value, this.vm, paramName) method in the compiled template, which is a method in the utility class used to process instructions

Let’s look at the text node, the text node is relatively simple, just need to match the interpolation expression in the form of {{}}, the same call tool method, to parse.

complieText(node) {
    //1. Get all the text content
    const text = node.textContent
    / / match {{}}
    if (/ \ {\ {(. +?) \} \} /.test(text)) {
        // Compile the template
        complieUtils['text'](node, text, this.vm)
    }
}
Copy the code

With all these tools and methods, let’s see what they are

5. Implement the complieUtils tool method

This method is mainly to process the instruction, get the value in the instruction, and update the corresponding value in the page, and we will bind watcher’s callback function here.

I’m going to use v-text instructions to explain, other instructions are annotated, you see.

import Watcher from './watcher'
export const complieUtils = {
    // Process the text instruction
    text(node, expr, vm) {
        let value;
        if (/ \ {\ {+? \} \} /.test(expr)) {
            / / {{}}
            value = expr.replace(/ \ {\ {(. +?) \}\}/g.(. args) = > {
                // Bind the observer/update function
                new Watcher(vm, args[1].() = > {
                    // The second argument is passed to the callback function
                    this.updater.updaterText(node, this.getContentVal(expr, vm))
                })
                return this.getValue(args[1], vm)
            })
        } else {
            //v-text
            new Watcher(vm, expr, (newVal) = > {
                this.updater.updaterText(node, newVal)
            })
            // Get the value
            value = this.getValue(expr, vm)
        }
        // Call update function
        this.updater.updaterText(node, value)
    },
}
Copy the code

The text handler operates on the textContent of the DOM element, so there are two cases: one is to update the textContent of the element with the V-text instruction, and the other is to update the textContent of the element with the interpolation of {{}}.

So in this method we’re going to determine which case, if it’s a V-text instruction, we’re going to bind a Watcher callback, get the value of the textContent, call updater.updaterText, which is the method that updates the element, as we’ll see below. If it is a double brace, we do something special to it, first replacing the double brace with the value of the specified variable and binding watcher’s callback to it.

// Through the expression, the VM gets the value in data, person.name
getValue(expr, vm) {
    return expr.split(".").reduce((data, currentVal) = > {
        return data[currentVal]
    }, vm.$data)
},
Copy the code

We get the value of textContent using a reduce function, which is used in the link at the bottom, because the data might be Person. name we need to get the value of the deepest object.

 // Update the dom element method
updater: {
    // Update the text
    updaterText(node, value) {
        node.textContent = value
    }
}
Copy the code

UpdaterText updates the DOM by reassigning textContent.

Let’s go back to the V-Model directive to implement two-way data binding. As we all know, v-Model actually implements the syntax sugar between the input event and the value. So here we also listen for the input event of the current DOM element and call the method that sets the new value when the data changes

// Process the model directive
model(node, expr, vm) {
    const value = this.getValue(expr, vm)
    / / bind watcher
    new Watcher(vm, expr, (newVal) = > {
        this.updater.updaterModel(node, newVal)
    })
    // Two-way data binding
    node.addEventListener("input".(e) = > {
        // Set the value method
        this.setVal(expr, vm, e.target.value)
    })
    this.updater.updaterModel(node, value)
},
Copy the code

This method also uses the reduce method to set the corresponding variable to a new value. When the data is changed, the method to update the view will be automatically called. We have implemented this method before.

// Through the expression,vm, input box value, implement the set value,input v-model two-way data binding
setVal(expr, vm, inputVal) {
    expr.split(".").reduce((data, currentVal) = > {
        data[currentVal] = inputVal
    }, vm.$data)
},
Copy the code

6. Implement VUE

Finally, we need to integrate these classes and utility methods. When creating a Vue instance, we first get the parameters in options, and then do data hijacking and template compilation

class Vue {
    constructor(options) {
        // Get the template
        this.$el = options.el;
        // Get data from data
        this.$data = options.data;
        // Save attributes in the object for later use
        this.$options = options
        //1. Data hijacking, set setter/getter
        new Observer(this.$data)
        //2. Compile the template and parse the instructions
        new Complie(this.$el, this)}}Copy the code

For example, if we want to use person.name in the VM object, we must use this.$data.person.name. If we want to use this.person.name in the VM object to modify the data directly, We need to delegate this.$data. The value of this.$data is placed globally.

export default class Vue {
    constructor(options) {
        / /...
        //1. Data hijacking, set setter/getter
        //2. Compile the template and parse the instructions
        if (this.$el) { // If there is a template
            / / agent for this
            this.proxyData(this.$data)
        }
    }
    proxyData(data) {
        for (const key in data) {
            // Put the current data in the global pointer
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                },
                set(newVal) {
                    data[key] = newVal
                }
            })
        }
    }
}
Copy the code

Article here, on the realization of a simple version of VUE, we recommend repeated learning, careful experience, savor.

At the end of the article, I will answer some common interview questions by asking and answering them:

Q: When will the page be re-rendered?

A: When the data changes, the page is re-rendered, but data-driven views, the data must exist before data binding can be implemented, and the page is re-rendered if the data changes.


Q: When is the page not re-rendered?

A: There are three cases where it will not be re-rendered

  1. Undeclared and unused variables will not be re-rendered if they are modified

  2. Changing the array by indexing and changing the length does not re-render the page

  3. Adding and removing properties of objects does not re-render the page


Q: How can undeclared/unused variables, adding/removing object attributes be used to render the page again?

$set/Vue. Set; $delete/Vue


Q: How can I change the array to make the page rerender?

A: You can mutate arrays with seven methods: Push, POP, unshift, Shift, splice, sort, reverse


Q: Will the page be re-rendered as soon as the data is updated?

A: The page is not rerendered immediately after the data is changed. The page rendering is performed asynchronously, only after the synchronization task is performed

Synchronous queue, asynchronous queue (macro task, micro task)


Q: What if you change the data and want to wait until the page is re-rendered?

A: You can use vm.$nextTick or vue. nextTick


Q: Let’s introduce vm.$nextTick and vue. nextTick

A: Let’s take a quick example

<div id="app">{{ name }}</div> <script> const vm = new Vue({ el: '#app', data: { name: 'monk' } }) vm.name = 'the young monk'; console.log(vm.name); // The young Monk has changed console.log(vm.$el.innerhtml); $nextTick vm.$nextTick(() => {console.log(vm.$el.innerhtml); // The young monk has been changed}) // 2. NextTick vue.nexttick (() => {console.log(vm.$el.innerhtml); // The young monk has been changed}) </script>Copy the code

Question: What is the difference between vm.$nextTick and vue. nextTick?

A: Vue.nextTick internal function this points to window, vm.$nextTick internal function this points to Vue instance object.

Vue.nextTick(function () {
	console.log(this); // window
})
vm.$nextTick(function () {
	console.log(this); / / vm instances
})
Copy the code

Question: How are vm.$nextTick and vue. nextTick implemented?

A: Both tasks are performed after the page has been rendered, and both use microtasks.

  if(typeof Promise! = ='undefined') {
    Micro / / task
    // First look to see if there are promises in the browser
    // Promise cannot be implemented in IE
    const p = Promise.resolve();

  } else if(typeofMutationObserver ! = ='undefined') {
    Micro / / task
    // Mutation observation
    // Listen for text changes in the document. If the text changes, a callback is executed
    // Vue creates a dummy node, and then lets the dummy node change slightly to execute the corresponding function
  } else if(typeofsetImmediate ! = ='undefined') {
    / / macro task
    // Only available in IE
  } else {
    / / macro task
    // If none of the above can be done, setTimeout is called
  }
Copy the code

This is also a minor drawback of Vue: Vue always waits for the main thread to finish rendering, and if the main thread freezes, it never renders.


Q: What are the disadvantages of using Object.defineProperty to implement responsiveness?

A:

  1. It’s natural to recurse
  2. Cannot listen for index changes that do not exist in the array
  3. Can’t listen for array length changes
  4. The object cannot be added or deleted