Work process analysis of Vue

We can get an idea of how Vue works from the diagram below.

We can analyze the working process of Vue from two aspects: initialization phase and data modification phase.

During the Vue initialization phase, we created a Vue instance and mounted it on the page:

  • During instance creation, we call oneinit()Methods. What did it do? It initializes the props, events, data, and so on passed in.
  • We do this by calling$mount()Method to achieve Vue instance mount. this$mount()Method, what is the main thing to do? It does this by callingrender()The function generates the virtual DOM, or virtual DOM tree.render()When the function is executed, it willtouchThe following corresponds to the propertygetter, this step is the triggergetterThe process of dependency collection.
  • Finally, callpatch()Method to generate a real DOM, mounted on the page.

In the data modification phase:

  • Data modification will trigger the corresponding propertysetter.
  • Because of data responsiveness, the corresponding listener Watcher performs an update operation.
  • By calling thepatch()Method, compare the old and new Virtual DOM, get the minimum modification of the page, and perform the page refresh.

Handwriting Vue includes features

I wanted to try implementing a simple Vue myself. What will it look like:

  • Inclusion: It will include core processes such as data responsiveness, dependency collection, and data update.
  • Parsing phase: Parsing only the simplest text custom variables{{}}.
  • Not included: There is no virtual DOM module and no Patch algorithm. One variable corresponds to one Watcher way (Vue 1 phase).

There will be five files:

  • Test the file index.html
  • The core of fVue. Js
  • Monitor watcher. Js
  • The scheduling module dep.js
  • The compiler compier. Js

First, give index.html as a test:


      
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
        {{test}}
        <p k-text="test"></p>
        <p k-html="html"></p>
        <p>
            <input type="text" k-model="test">
        </p>
        <p>
            <button @click="onClick">button</button>
        </p>
    </div>

    <script src="fvue.js"></script>
    <script src="fcompile.js"></script>
    <script src="watcher.js"></script>
    <script src="dep.js"></script>
    <script>
      const fVue = new FVue({
        el: "#app".data: {
          test: "hello, frank".foo: { bar: "bar" },
          html: '<button>adfadsf</button>'
        },
        methods: {
            onClick() {
                alert('blabla')}}});// Simulate data modification
      setTimeout(function(){
        fVue.$data.test = "hello,fVue!";
        console.log("setTimeout : ",fVue.$data.test);
      }, 2000);
    </script>
  </body>
</html>
Copy the code

Code implementation

To test the idea, these four documents were written. Keep the code simple.

//fvue.js
class FVue {

    constructor(options){
        this.$data = options.data;
        this.$options = options;
        // Data reactivity
        this.observe(this.$data);
        // Parse the page template
        new Compile(options.el, this);
    }

    observe(value){
        if(! value ||typeofvalue ! = ='object') {return;
        }
        Object.keys(value).forEach(key= >{
            this.defineReactive(value, key, value[key]);
            // Proxy vue data: this. XXX = this.$data.xxx
            this.proxyData(key);
        })
    }
    
    defineReactive(obj, key, val){
        / / recursion
        this.observe(val);
        // Each key has a Dep corresponding to it
        const dep = new Dep();

        Object.defineProperty(obj, key, {
            get(){
                // Rely on collection
                Dep.target &&  dep.addDep(Dep.target)
                return val;
            },
            set(newVal){
                if(newVal === val) return;
                val = newVal;
                // Perform the update operation
                dep.notify();
            }
        })
    }

    proxyData(key) {
        Object.defineProperty(this, key, {
            get(){
                return this.$data[key];
            },
            set(newVal){
                this.$data[key] = newVal; }}); }}Copy the code

The fvue. Js core file implements observe logic: during initialization, the incoming data attribute is initialized, data is intercepted for each attribute in data through defineReactive() method, and getter and setter of each attribute are redefined. More detailed:

  • Each attribute has its own proprietary scheduling module, Dep.

  • In the getter, the dependency collection method is defined (as long as the corresponding Watcher triggers the getter method, it is put into the ARRAY of the Dep).

  • In the setter, methods are defined to respond to changes in data (as soon as the corresponding setter method is fired, the Dep will perform notification operations to make the corresponding Watcher perform updates).

Dep.js vs. Watcher.js.

//dep.js
class Dep {
    constructor() {this.deps = []
    }

    addDep(dep){
        this.deps.push(dep)
    }

    notify(){
        this.deps.forEach(dep= > dep.update())
    } 
}

//watcher.js
class Watcher{
    constructor(vm, key, cb){
        this.vm = vm;
        this.key = key;
        this.cb = cb;

        Dep.target = this; // Append the current Watcher instance to the static property of the Dep
        this.vm[this.key]; // Trigger the getter property to trigger the dependency collection
        Dep.target = null; // Unlock the dep. target static variable
    }

    update(){
        this.cb.call(this.vm, this.vm[this.key]); }}Copy the code

We think of Dep as a scheduling module that only manages updates. Watcher, on the other hand, acts as an enforcer, executing the specific update process.

We see that during Watcher initialization, we actively trigger the getter property, triggering the dependency collection process. However, I haven’t seen where Watcher was initialized yet. In fact, while parsing the HTML template, Watcher initialization is triggered when we find a custom variable.

For simplicity, verify feasibility. At this point our fcompile.js will be written very simply, handling only the case of text custom variables (in this case {{test}}).

class Compile {
    //el is the host element or selector
    // VM is a vue instance
    constructor(el, vm){
        this.$vm = vm;
        this.$el = document.querySelector(el); // Simplify: use the selector to get the document element

        this.compile(this.$el);
    }

    compile(el){
        const childNodes =  el.childNodes;
        Array.from(childNodes).forEach(node= > {
            if(this.isTextParam(node)){
                this.compileText(node);
            }
            / / recursion
            this.compile(node);
        })
    }

    isTextParam(node){
        return node.nodeType === 3 && / \ {\ {(. *) \} \} /.test(node.textContent);
    }

    compileText(node){
        let key = RegExp. $1;
        let currentValue = this.$vm[key];
        // After parsing, you need to mount the real value to the real page
        this.textUpdate(node, currentValue)
        // Create a new Watcher instance
        new Watcher(this.$vm, key, (newValue)=>{
            this.textUpdate(node, newValue) }) } textUpdate(node, value){ node.textContent = value; }}Copy the code

Compile is called in FVue. It has the most to do:

  • Parse HTML templates, find out all kinds of custom variables, events, etc., and display the real values corresponding to the custom variables on the web page.
  • Most critically: create a new Watcher instance to trigger dependency collection. At the same time, it responds to the update of Watcher in real time and displays the latest data responsive results on the corresponding position of the page.

Of course, for the sake of simplicity, Compile here only handles the simplest case: the case of a text custom variable ({{test}}). A complete compile function will be very thorough and complex, check Vue source code.

conclusion

Put the code together and it works. Display variables on the page will change after the timer time expires.

At the end of the article, let’s take a look at how Vue works:

  • During initialization, Observe intercepts each property of incoming data and sets the data responsivity logic.
  • In the template parsing phase, Compile triggers dependency collection by looking for custom variables, events, etc., and creating a new Watcher instance for this purpose.
  • When data changes, the setter on the property triggers the notification operation for the corresponding Dep, causing the corresponding Watcher instance to perform the update.
  • When Watcher performs an update, the custom variables on the HTML template change with it. This triggers a page refresh.

The whole process can be seen as an extremely simple version of how Vue 1.x works, which is different from Vue 2.x, but hopefully won’t affect your understanding of Vue.