Hello everyone, I am Wang Dashao. Recently, when I was doing sharing in the company, I found that everyone did not fully understand the responsive principle of Vue, so I simply explained the responsive principle to colleagues in the company and got some insights. Therefore, I would like to share a wave of this article (this is the first time to write an article, please leave a message and tell me if there is anything not in place).

Analysis of the

Well, first of all, we can look at this diagram of the principle of responsiveness. When we create the Vue instance, we do two things

  1. The data was hijacked
  2. Parse command

In the process of data hijacking, after data hijacking, we will timely feedback the changes of data to our Dep instance. At this time, the Dep(target publisher) will notify our Watcher(observer), and our Watcher will call relevant functions to update the view after receiving the notification. At this point, data hijacking is also completed. Wait, we have another line. In the external parsing route, we first sent the instructions to our Compiler for parsing. After that, we directly rendered to the view layer to replace our previous interpolation expressions, subscribed to specific changes in the data, and bound our update function. At this point, Watcher would add subscribers to our Dep.

Observer model

Before we talk about how it works, we need to understand what the observer model is.

As shown in the figure, the observed (our publisher) can support multiple observers at the same time, while the observed is mainly used to register our observation instances and notify our observers of the results immediately.

// # publisher
class Dep {
    constructor() {
        this.subs = []// Store our observer
    }

    addSub(sub) {
        if (sub && sub.update) {// Check whether the observer function update is the observer function
            this.subs.push(sub)
        }
    }

    notify() {
        this.subs.forEach(sub= > {
            sub.update()// Execute our observer function}}})// # observer
class Watcher {
    constructor() {
        update()
        {
            console.log('update')}}}let dep = new Dep()
let watch = new Watcher()
dep.addSub(watch)// Register our observation instance
dep.notify()  // Notify our observer

Copy the code

Now that our observer mode is clear, let’s think about our specific use in Vue

The first step is to create the Vue class

The first, most important thing is that we need to initialize a Vue class

let vm=new Vue({
    el:'#app'.data: {msg:'hello'.title:'hello'}})console.log(vm)
Copy the code

The Vue here is an instance we created ourselves, so let’s look at it first

  1. The Vue instance accepts an object
  2. The object contains two parameters, el and data
  3. So what does Vue do
    1. Responsible for receiving initialization parameters
    2. Is responsible for injecting properties from Data into Vue instance getters/setters
    3. Responsible for calling the Observer to listen for changes to all properties in data
    4. Responsible for calling compiler to parse instructions/expressions

So let’s initialize the instance based on what we know about the function

class Vue {
    constructor(options) {
        //1. Save the data of the option through properties
        this.$options = options
        this.$data = options.data || {}
        this.$el = typeof options.el === "string" 
                        ? document.querySelector(options.el) 
                        : options.el
        //2. Convert data members into getter setters and inject them into vue instances
        this._proxyData(this.$data)
        //3. Call an Observer to listen for data changes
        new Observer(this.$data)
        //4. Invoke compiler object parsing instructions and interpolation expressions
        new Compiler(this)}_proxyData(data) {
        // Iterate over all attributes in data
        Object.keys(data).forEach(key= > {
            // Inject the data attribute into the vue instance
            Object.defineProperty(this, key, {
                enumerable: true.configurable: true.get() {
                    return data[key]
                },
                set(newValue) {
                    if (newValue === data[key]) {// Check whether the imported value is equal to the current value
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
}
Copy the code

Step 2 Create an Observer

Next, we need to create an Observer class that sets setters and getters for the data property in our Vue, so let’s examine its capabilities

  1. Is responsible for converting properties in the Data option to reactive data
  2. A property in data is also an object that converts that property into responsive data
  3. Send notifications of data changes

Let’s implement it according to its function

class Observer {
    constructor(data) {// Receive the data we passed in the Vue instance
        this.walk(data)
    }

    walk(data) {
        // 1. Check whether data is an object
        if(! data ||typeofdata ! = ='object') return
        // 2. Iterate over all attributes in data
        Object.keys(data).forEach(key= > {
            this.defineReactive(data, key, data[key])
        })
    }

    defineReactive(data, key, value) {
        const self = this// Save our pointer so that this points to our instance
        let dep = new Dep()// Create a publisher instance first
        this.walk(value)// If it is an object, it will listen on the object as well
        Object.defineProperty(obj, key, {
            enumerable: true.configurable: true.get() {
            // Determine if our Dep instance has a target attribute and add target to our publisher store
             Dep.target && dep.addSub(Dep.target)
            // We currently reserve this code for later adding the target attribute to the Dep in Watcher
                return value
            },
            set(newVal) {
                if (newVal === value) {
                    return
                }
                value = newVal
                self.walk(newVal)
                dep.notify()// Send notifications when we go to set new values}}}})Copy the code

At this point we can print our VM instance on the console

  1. You need to annotate the currently undeclared classes and their associated use, such as the Dep in Compiler Observer in Vue

Step 3 Create our Compiler class

Now that we have initialized the Vue instance and used the Observer to mount the data from the instance to our Vue, we have added getters and setters to the Vue, and now we can focus on the Compiler class.

  1. Responsible for compiling templates, parsing instructions and interpolation
  2. Responsible for the first rendering of the page
  3. Re-render the view when the data changes

When it comes to compiling templates, we need to do some work on tags. Here we implement interpolation and v-text V-model padding

So here’s what we need to do

  1. Replace the interpolation part with the concrete data in our data
  2. The variables in the V-text binding are replaced by the concrete data in our data
  3. The form data bound by the V-Model is also replaced with concrete data, and changes to the data are also timely in response to the page.

We create the Compiler class based on the above

  class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
    }

    // Compile templates to handle text and element nodes
    compile(el) {
        let childNodes = el.childNodes
        Array.from(childNodes).forEach(node= > {
            if (this.isTextNode(node)) {
                // Determine the text node
                this.compileText(node)
            } else if (this.isElementNode(node)) {
                // Determine the element node
                this.compileELement(node)
            }
            if (node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }

    // Compile element node processing instructions
    compileELement(node) {
        Array.from(node.attributes).forEach(attr= > {
            let attrName = attr.name
            if (this.isDerictive(attrName)) {
                attrName = attrName.substr(2)
                let key = attr.value
                this.update(node, key, attrName)
            }
        })
        // Iterate over all nodes
        // Check if it is a command
    }

    // Determine which method needs to be executed
    update(node, key, attrName) {
        let fn = this[attrName + 'Update']
        fn && fn.call(this, node, this.vm[key], key)
        // We need call to change our this pointer because the this pointer points to the window object when we store the function as a variable
    }

    // Process the V-text instruction
    textUpdate(node, val, key) {
        node.textContent = val
        new Watcher(this.vm, key, (newVal) = > {
            node.textContent = newVal
        })
    }

    // Process the V-model directive
    modelUpdate(node, val, key) {
        node.value = val
        new Watcher(this.vm, key, (newVal) = > {
            node.value = newVal
        })
        // Bidirectional binding
        node.addEventListener('input'.() = > {
            this.vm[key] = node.value
        })
    }

    // Compile text nodes to handle interpolation expressions
    compileText(node) {
        let reg = / \ {\ {(. +?) \} \} /
        let val = node.textContent
        if (reg.test(val)) {
            let key = RegExp.$1.trim()
            node.textContent = val.replace(reg, this.vm[key])
            // Create a Watcher object to update the view when its properties change
            new Watcher(this.vm, key, (newVal) = > {
            // Write our watcher method to add our observer while processing the node
                node.textContent = newVal
            })
        }
    }

    // Determine if the element is an instruction
    isDerictive(attrName) {
        return attrName.startsWith('v-')}// Determine whether the node is a text node
    isTextNode(node) {
        return node.nodeType === 3
    }

    // Determine if it is an element node
    isElementNode(node) {
        return node.nodeType === 1}}Copy the code

Some of the decisions in the code will be explained separately

  1. We need to determine two types of nodes (text nodes and element nodes)
  2. Nodetype ===3 in the text node and can be assigned via textContent
  3. In the element node we say nodeType ===1 if it’s a form element then we can assign it by value if it’s not we can get attributes and assign it by textContent

Text node

Element nodes

So far the interpolation and v-text V-model on our page have been converted to our corresponding data

Step 4 Create our observer mode to complete the final operation

We’ve covered the observer model before, so without further ado, we’ll go straight to our publishers first

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

    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }

    notify() {
        this.subs.forEach(sub= > {
            sub.update()
        })
    }
}
Copy the code

And then our observer so in our observer we need the following function based on what we’re doing at this point

  1. Trigger relies on deP to notify Watcher instances to update views when data changes
  2. Add yourself to the DEP when you change yourself
 class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb
        // The current watcher is recorded in the deP target
        // Trigger the get method addSub in get
        Dep.target = this
        this.oldValue = vm[key]
        Dep.target = null// Recycle target at the end to avoid multiple additions
    }

    update() {
        let newValue = this.vm[this.key]
        if (this.oldValue === newValue) {
            return
        }
        this.cb(newValue)
    }

}
Copy the code

Now that our Vue responsive principle is complete, let’s run it through the page

First we introduce files into the HTML page in the order in which the classes depend on each other

The result we run into the pageBidirectional binding is triggered when we enter data in the input fieldOver, so far we understand how a simpler version of the reactive principle works. If you have any questions, feel free to leave them in the comments section. I will answer them one by one. If there is any piece that is not good, you are welcome to correct it