vue implementation principle and simple example implementation

Creation time: 2020-09-11

online demo

Mainly understand and implement the following methods:

  • Observe : a listener that listens for property changes and notifies subscribers
  • Watch : Subscribers receive property changes and update the view
  • Compile : parser parsing directives, initializing templates, binding subscribers, binding events
  • Dep : store all corresponding watcher instances

Main execution process

The process of loading the watcher into the subscription list of the corresponding dep instance

Relevant html code for parsed binding data

The code here is the template to be parsed in compile, parsing the commands such as v-model {{}} v-on

<div id="app">
    <p> Name:<input type="text" v-model="name"></p>
    <p>student ID:<input type="text" v-model="number"></p>
    <p><span>student ID:</span> <span>{{ number }}</span></p>
    <p><span>computed accomplish:</span> <span>{{ getStudent }}</span></p>

    <p>
        <button v-on:click="setData">event binding</button>
    </p>
</div>

observer code

Add get, set for data data to add watcher, and create Dep instance to notify update view

const defineProp = function(obj, key, value) {
    observe(value)

    /*
    * Create a unique dep for each different property in advance
    */
    const dep = new Dep()
    Object.defineProperty(obj, key, {
        get: function() {
            /*
            * Created according to different properties and only called when the Watcher is created
            */
            if(Dep.target) {
                dep.targetAddDep()
            }
            return value
        },
        set: function(newVal) {

            if(newVal !== value) {
                /*
                * The assignment operation here is to facilitate the return of the value in the get method, because the get method will be called immediately after the assignment
                */
                value = newVal
                /*
                * Notify all subscribers of the listening property
                */
                dep.notify()
            }
        }
    })
}

const observe = function(obj) {
    if(!obj || typeof obj !== 'object') return

    Object.keys(obj).forEach(function(key) {
        defineProp(obj, key, obj[key])
    })
}

Dep code

Mainly put the watcher into the corresponding dep subscription list

let UUID = 0
function Dep() {
    this.id = UUID++
    // Store all listening watcher s of the current property
    this.subs = []
}
Dep.prototype.addSub = function(watcher) {
    this.subs.push(watcher)
}
// The purpose is to pass the current dep instance to the watcher
Dep.prototype.targetAddDep = function() {
    // Here this is the instantiated dep
    Dep.target.addDep(this)
}
Dep.prototype.notify = function() {
    // Fire all watcher s for the current property
    this.subs.forEach(_watcher => {
        _watcher.update()
    })
}
Dep.target = null

Watcher code

After the data is updated, update the view

function Watcher(vm, prop, callback) {
    this.vm = vm
    this.prop = prop
    this.callback = callback
    this.depId = {}
    this.value = this.pushWatcher()
}

Watcher.prototype = {
    update: function() {
        /* update value change */
        const value = this.vm[this.prop]
        const oldValue = this.value
        if (value !== oldValue) {
            this.value = value
            this.callback(value)
        }
    },
    // The purpose is to receive the dep instance, which is used to put the current watcher instance into subs
    addDep: function(dep) {
        if(!this.depId.hasOwnProperty(dep.id)) {
            dep.addSub(this)
            this.depId[dep.id] = dep.id
        } else {
            console.log('already exist');
        }
    },
    pushWatcher: function() {
        // Storage Subscriber
        Dep.target = this
        // Trigger the get monitoring of the object, and add this assigned to target above to subs
        var value = this.vm[this.prop]
        // delete after adding
        Dep.target = null
        return value
    }
}

Compile code

Parse html templates, create code snippets, bind data events

function Compile(vm) {
    this._vm = vm
    this._el = vm._el
    this.methods = vm._methods
    this.fragment = null
    this.init()
}
Compile.prototype = {
    init: function() {
        this.fragment = this.createFragment()
        this.compileNode(this.fragment)

        // When the content of the code snippet is compiled, insert it into the DOM
        this._el.appendChild(this.fragment)
    },

    // Create document fragments based on real DOM nodes
    createFragment: function() {
        const fragment = document.createDocumentFragment()
        let child = this._el.firstChild
        while(child) {
            // After adding a node to the document fragment, the node will be deleted from its original position, which is equivalent to moving the node position
            fragment.appendChild(child)
            child = this._el.firstChild
        }

        return fragment
    },

    compileNode: function(fragment) {
        let childNodes = fragment.childNodes;
        [...childNodes].forEach(node =>{
            if(this.isElementNode(node)) {
                this.compileElementNode(node)
            }

            let reg = /\{\{(.*)\}\}/
            // Get all text content under the node
            let text = node.textContent

            // Determine whether it is a plain text node and whether the text content has {{}}
            if(this.isTextNode(node) && reg.test(text)) {
                let prop = reg.exec(text)[1].trim()
                this.compileText(node, prop)
            }

            if(node.childNodes && node.childNodes.length) {
                // Compile child nodes recursively
                this.compileNode(node)
            }
        })
    },

    compileElementNode: function(element) {
        // Get attributes, only element nodes have the following methods
        let nodeAttrs = element.attributes;
        [...nodeAttrs].forEach(attr => {
            let name = attr.name

            if(this.isDirective(name)) {
                /*
                * v-model on a tag that accepts input events
                */
                let prop = attr.value
                if (name === 'v-model') {
                    /*
                    * The obtained value is the data that needs to be bound
                    */
                    this.compileModel(element, prop)
                } else if(this.isEvent(name)) {
                    /*
                    * bind event
                    */
                    this.bindEvent(element, name, prop)
                }
            }
        })
    },

    compileModel: function(element, prop) {
        let val = this._vm[prop]
        this.updateElementValue(element, val)
        new Watcher(this._vm, prop, value => {
            this.updateElementValue(element, value)
        })

        element.addEventListener('input', event => {
            let newValue = event.target.value
            if (val === newValue) return
            this._vm[prop] = newValue
        })
    },

    compileText: function(textNode, prop) {
        let text = ''
        if(/\./.test(prop)) {
            var props = prop.split('.')
            text = this._vm[props[0]][props[1]]
        } else {
            text = this._vm[prop]
        }

        this.updateText(textNode, text)

        console.log(text);

        new Watcher(this._vm, prop, (value) => {
            this.updateText(textNode, value)
        })
    },

    bindEvent: function(element, name, prop) {
        var eventType = name.split(':')[1]
        var fn = this._vm._methods[prop]
        element.addEventListener(eventType, fn.bind(this._vm))
    },

    /*
    * Determine if an attribute is an instruction
    */
    isDirective: function (text) {
        return /v-/.test(text)
    },

    isEvent: function(text) {
        return /v-on/.test(text)
    },

    isElementNode: function(node) {
        // element node returns 1 text node (text in element or attribute) 3 attribute node 2 (deprecated)
        return node.nodeType === 1
    },

    isTextNode: function(node) {
        return node.nodeType === 3
    },

    updateElementValue: function(element, value) {
        element.value = value || ''
    },

    updateText: function(textNode, value) {
        textNode.textContent = value || ''
    }
}

vue brief constructor

Mainly realizes two-way binding of data, custom events, computed

function FakeVue(options) {
    this._data = options.data
    this._methods = options.methods
    this._computed= options.computed
    this._el = document.querySelector(options.el)

    // Proxy the attributes in _data to the outer vm, here only the first layer attributes of _data are proxied
    Object.keys(this._data).forEach(key => {
        this._proxyData(key)
    })
    this._initComputed()
    this._init()
}
FakeVue.prototype._init = function() {
    // Begin to recursively listen to all properties of the object until the property value is a value type
    observe(this._data)
    new Compile(this)
}
FakeVue.prototype._proxyData = function(key) {
    Object.defineProperty(this, key, {
        get: function() {
            return this._data[key]
        },
        set: function (value) {
            this._data[key] = value
        }
    })
}
FakeVue.prototype._initComputed = function() {
    // Simple implementation: just mount the value to keep up
    const computed = this._computed
    Object.keys(computed).forEach((v) => {
        Object.defineProperty(this, v, {
            get: computed[v],
            set: function (value) {}
        })
    })
}

Create a vue instance

try{
    let vm = new FakeVue({
        el: '#app',
        data: {
            name: 'warren',
            number: '10011',
            score: {
                math: 90
            }
        },

        computed: {
            getStudent: function() {
                return `${this.name}: student number is ${this.number}`
            }
        },

        methods:{
            // By binding events to elements in compile
            setData: function() {
                alert('name: '+this.name);
            }
        }
    });
} catch(error) {
    console.error(error)
}

Epilogue

This is a simple example of vue implementation principle from the author's understanding point of view, I hope it will help you who are exploring

In this example, the main complication is the parsing of the html template, the two-way binding of the data.

It is recommended to follow the execution order of the code to understand the whole process. The code of key points has necessary comments. If you find any problems, please correct them.

Finally attach vue Source address , mainly concerned with the core and compiler files;

Welcome to exchange Github

Tags: Javascript Vue.js

Posted by aleigh on Tue, 17 May 2022 13:51:28 +0300