index.html

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
    <input type="text" v-model='value.a'>
    {{value.a}}
    <button @click='button'>Add 1</button>
    </div>
</body>
<script src="./mvvm.js"></script>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./dep.js"></script>
<script src="./compile.js"></script>

<script>
    var app = new mvvm({
        el:'#app'.data: {value: {a:'123'}},methods: {button(){
                this.value.a++
            },
        }
    })
</script>
</html>
Copy the code

mvvm.js

class mvvm {
    constructor(config) {
        // Bind the template element node to the instance's $el attribute
        this.$el = document.querySelector(config.el)
        if (!this.$el) {
            throw new Error('Component DOM root element cannot be empty')}this.$data = config.data
        this.methods = config.methods
        // Data broker
        this.proxyData(config.data)
        // Data monitoring
        new Observer(config.data)
        // Compile the template
        new Compile(this.$el, this)}// Proxy methods
    proxyData(data) {
        Object.keys(data).forEach(key= > {
            Object.defineProperty(this, key, {
                configurable: true.get() {
                    return this.$data[key]
                },
                set(newValue) {
                    this.$data[key] = newValue
                }
            })
        })
    }
}

Copy the code

observer.js

class Observer {
    constructor(data) {
        this.data = data
        this.observer(data)
    }
    observer(data) {
        if(! data ||typeofdata ! = ='object') { return }
        Keys returns an array of the keys of the Object
        // Loop through object.defineProperty to listen for property changes
        Object.keys(data).forEach(key= > {
            this.defineReactive(data, key, data[key]) // Data monitoring
            // If data[key] is an object that contains objects, continue to listen for deeper data
            this.observer(data[key])
        })
    }
    defineReactive(data, key, value) {
        let dep = new Dep()
        Object.defineProperty(data, key, {
            configurable: true.enumerable: true.get: () = > {
                // The subscriber Dep is designed to add a subscriber in the getter so that Watcher initialization triggers it, so it needs to decide whether to add a subscriber.
                Dep.target && dep.addSub(Dep.target)
                console.log('Got data' + value, key);
                return value
            },
            // The argument to set is the new value assigned to the property
            set: (newValue) = > {
                // Replace the old value
                if(newValue ! = value) {console.log('Updated data' + newValue);
                    // The new value also needs to be listened again
                    this.observer(newValue)
                    value = newValue
                    // Trigger watcher update()
                    dep.notify()
                }
            }
        })
    }
}
Copy the code

watcher.js

class Watcher {
    constructor(vm, exp, cb) {
        this.vm = vm
        this.exp = exp{{value. A}}} exp is value. A.
        this.cb = cb
        // Store a value
        this.oldValue = this.get()
    }
    get() {
        Dep.target = this // Cache the watcher instance to dep.target
        // Get the data on the VM and get the getter for the data object, which reads the Watcher instance from dep. target and adds it to the Dep
        // let value = this.vm.$data[this.exp
        let value = this.getValue(this.vm, this.exp)
        Dep.target = null
        return value
    }
    update() {
        // let newValue = this.vm.$data[this.exp]
        let newValue = this.getValue(this.vm, this.exp)
        if (this.oldValue ! == newValue) {this.oldValue = newValue
            this.cb(newValue) // Execute the callback function}}getValue(vm, exp){ exp = exp? .split('. ') // [message.a]
        returnexp? .reduce((prev, next) = > {
            return prev[next]
        }, vm.$data)
    }

}
Copy the code

dep.js

class Dep {
    constructor() {
        // Use an array to store watcher
        this.subs = []
    }
    addSub(watcher) {
        // Save watcher
        this.subs.push(watcher)
    }
    notify() {
        // Call update() to find the corresponding watcher when updating the value
        this.subs.forEach(watcher= > {
            watcher.update()
        })
    }
}
// This is a global Watcher. Only one global Wathcer can be calculated at a time
Dep.target = null
Copy the code

compile.js

class Compile {
    constructor(el, vm) {
        this.el = el
        this.vm = vm
        // Generate a virtual DOM (representing nodes as objects or arrays)
        this.complierNodes();
        // Regenerate nodes based on the virtual DOM
        this.createElement()
    }
    // Generate the virtual DOM
    complierNodes() {
        // All children of the dom element of the root node
        var nodeList = this.el.childNodes;
        // Generate vmNodes virtual DOM
        this.vm.vmNodes = this.complierNodesChild(nodeList)
    }
    // Generate the virtual DOM function recursively
    complierNodesChild(nodeList) {
        // Initialize the vmNodes array, which is finally used to return
        var vmNodes = []
        // Use an object to concatenate our data
        var data = {}
        //nodeList is a pseudo-array and cannot be traversed directly
        Array.from(nodeList).forEach(node= > {
            // Matches the contents of the interpolation in the string
            var reg = /\{\{[^\{\}]*\}\}/g;
            data = {
                node: node,// Store node elements
                nodeName: node.nodeName,// The node name
                nodeValue: node.nodeValue,// The element value
                nodeType: node.nodeType,// The node type of the element, 1 for normal elements, 3 for text nodes, and 8 for comments
                data: [].// Store the contents of the interpolation expression in the node
                attrs: node.attributes,// Attributes of the element node
                props: {},// All attributes except those beginning with v-
                directives: {},// An array of instructions
                children: [].// To store child nodes
                events: {},// Store node events
            }
            // If the current node is a text node
            if (node.nodeType === 3) {
                // if the nodeValue value is null, the value is returned
                if (node.nodeValue.trim() === ' ') {
                    return false
                } else {
                    // The match method of the string returns an array based on the re
                    var arr = node.nodeValue.match(reg) || [];
                    // Loop through the braces
                    arr.forEach(v= > {
                        v = v.replace(/[/{/}]/g."");
                        data.data.push(v)
                    })
                }
            }
            // If the current node is a normal node
            if (node.nodeType === 1) {
                // Get the attributes of the element node
                varattrObj = { ... node.attributes }Object.keys(attrObj).forEach(index= > {
                    var prop = attrObj[index]
                    // Determine if the attribute starts with a v-
                    if (/^(v-)+/.test(prop.name)) {
                        // Save the directives to data's directives
                        data.directives[prop.name] = prop.value
                    } else if (+ / / ^ (@).test(prop.name)) {
                        // If the attribute starts with @, it is an event
                        data.events[prop.name.replace(The '@'.' ')] = prop.value
                    } else {
                        // Add attributes that do not start with v- to data.props
                        data.props[prop.name] = prop.value
                    }
                })
            }
            // If the node has children, the current function is executed recursively
            if (node.childNodes.length > 0) {
                data.children = this.complierNodesChild(node.childNodes)
            }
            // Put objects on virtual nodes
            vmNodes.push(data)
        })
        return vmNodes
    }
    /** * reduce executes callbacks for each element in the array, excluding elements that were deleted or never assigned. * Takes four arguments: the initial value (or the value returned by the last callback), the current element value, the current index, and the array that called Reduce. * /
    // Use the reduce function to get the lowest level key
    getValue(exp){ exp = exp? .split('. ')
        returnexp? .reduce((prev, next) = > {
            return prev[next]
        }, this.vm.$data)
    }
    getInputSetValue(vm, exp, value){ exp = exp? .split('. ')
        returnexp? .reduce((prev, next, currentIndex) = > {
            $data = vm.$data = vm.$data = vm
            if (currentIndex == exp.length - 1) {
                prev[next] = value
            }
            return prev[next]
        }, vm.$data)
    }
    // Generate child nodes recursively
    createElementChild(parentNode, nodeList) {
        // Loop through the virtual node to regenerate the DOM element
        nodeList.forEach(node= > {
            var newNode;
            // Use createElement for element nodes (non-text nodes)
            if (node.nodeType === 1) {
                newNode = document.createElement(node.nodeName)
                // Element nodes may have time to bind events using addEventListener
                Object.keys(node.events).forEach(eventName= > {
                    newNode.addEventListener(eventName, (event) = > {
                        // The "this" in methods refers to the VM instance
                        // this.vm.methods[node.events[eventName]](event);
                        this.vm.methods[node.events[eventName]].call(this.vm, event); })})// If the current node is an input element, the input element also contains v-model
                if (node.nodeName == 'INPUT' && node.attrs['v-model']) {
                    // Get the attribute value of vm.$data to assign the current node value
                    let value = this.getValue(node.attrs['v-model'].value)
                    newNode.value = value
                    // Listen for changes in the data property of the V-model binding
                    new Watcher(this.vm, node.attrs['v-model'].value, () = > {
                        newNode.value = this.getValue(node.attrs['v-model'].value)
                    })
                    // Handle the input event for the input tag
                    newNode.addEventListener('input'.(event) = > {
                        // Get the value of the input box
                        var inputValue = event.target.value;
                        // Modifying an attribute under vm.$data triggers the set method to update the page
                        this.getInputSetValue(this.vm, node.attrs['v-model'].value, inputValue)
                    })
                }
            }
           // Text node
            if (node.nodeType === 3) {
                // Replace the interpolation content of the node with the attribute value of vm.$data
                var text = this.replaceElementText(node.nodeValue);
                newNode = document.createTextNode(text)
                // Listen for vm.$data to change the current node attribute value
                new Watcher(this.vm, node.data[0].() = > {
                    node.node.nodeValue = this.replaceElementText(node.nodeValue)
                })
            }
            // Comment the node
            if (node.nodeType === 8) {
                return
            }
            // Overwrite the original DOM node with the new DOM node
            node.node = newNode
            parentNode.appendChild(newNode)
            if (node.children.length > 0) {
                // Determine if there are child elements, the central execution function, the function will also change relative
                this.createElementChild(newNode, node.children)
            }
        })
    }
    // Replace the interpolation content of the node with the attribute value of vm.$data
    replaceElementText(value) {
        // Global regular expression
        var reg = /\{\{[^\{\}]*\}\}/g;
        // Return the data with two braces to an array
        var regArr = value.match(reg);
        // If the value is an array, there are two braces inside the value
        if (Array.isArray(regArr)) {
            // The loop replaces the value of value
            regArr.forEach(v= > {
                {{value.a}}} {value
                var prop = v.replace(/[/{/}]/g."");
                value = this.getValue(prop)
            })
        }
        return value;
    }
    // Regenerate nodes based on the virtual DOM
    createElement() {
        // Create a virtual template node (document fragment)
        var fragment = document.createDocumentFragment();
        // Returns the DOM element generated by the last virtual node
        this.createElementChild(fragment, this.vm.vmNodes);
        // Clear all contents of the root node
        this.el.innerHTML = ' ';
        // Regenerate the DOM element
        this.el.appendChild(fragment)
    }
}
Copy the code

Effect demonstration: