preface

With the hot development of Vue, more and more programmers are not satisfied with the use of the framework, more to pursue its internal principle, just like can not sink into the beautiful appearance, should pursue the height of the soul.

The body of the

Well, without further ado, we’re going to pursue our external, nay, internal, pursuits in two ways.

1 Understand the principle of vUE bidirectional data binding

2 after understanding the principle, the interesting soul for a wave of shaping, simple implementation of an MVVM framework

Vue implements bidirectional data binding

Vue. js adopts the mode of data hijacking combined with the observer mode. Through Object.defineProperty(), it hijacks the setter and getter of each attribute, releases the message to the observer when the data changes, and triggers the corresponding listener callback.

Create an interesting soul

Now that we know about vUE bidirectional data, we can implement a simple MVVM framework. Now that we know that VUE is implemented through data hijacking combined with the observer pattern, we can use:

  • 1, implement a data listener Observer, can monitor all attributes of the data object, if there is any change can get the latest value and notify the Observer

  • 2. Compile an instruction parser, scan and parse the instructions of each element node, replace data according to the instruction template, and bind the corresponding update function

  • Implement a Watcher that acts as a bridge between the Observer and Compile. It receives notification of each property change and executes the corresponding callback function bound by the instruction to update the view

  • 4, implement an MVVM entry class, data hijacking entry

1 implementation of an MVVM entry class to achieve the first entry class, unified entry, and then through the acquisition of data for data hijacking and instruction parsing

class mVue {
    constructor (options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
            // 1. Implement a data observer
            new Observer(this.$data);
            // 2. Implement an instruction parser
            new Compile(this.$el, this); }}}Copy the code

Implement a data listener Observer class

Instantiate the Observer class, retrieve the value of each Object of data by recursively calling the Observe method, and hijack the get/set of data via Object.defineProperty. The defineReactive method creates a data dependent Dep as a closure, maintains a Dep for each property, records its own observer (that is, watcher), and notify notifies each observer to perform the corresponding update method to update the view

So the question is, right? How does the Observer class record its own Observer watcher?

Simply maintain an array subs that collects the watcher, triggers notify, and calls the update method of the observer

class Observer{
    constructor (data) {
        this.observe(data);
    }
    observe (data) {
        / * * {persion: {name: 'fanke fav: {a:' ball '}}} * /
        if(data && typeof data === 'object') {
            // Data is hijacked
            Object.keys(data).forEach( key= > {
                this.defineReactive(data, key, data[key])
            })
        }
    }
    defineReactive (data, key, value) {
        const _this = this;
        // recursive traversal
        this.observe(value);
        const dep = new Dep(); // The data dependency maintains a Dep for each property to record its own observer (i.e. watcher), and notify each observer to perform the corresponding update method to update the view
        Object.defineProperty(data, key, {
            enumerable: true./ / can be enumerated
            configurable: false.// cannot define again
            get() {
                // Add an observer to the Dep when data changes
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set (newValue) {
                _this.observe(newValue);
                if(newValue ! == value) { value = newValue;// Notifies the data dependent DEP, which notifies the observer of the updatedep.notify(); }})}}Copy the code
class Dep {
    // Collect + notification
    constructor () {
        this.subs = [];
    }
    // Collect observers
    addSub (watcher) {
        this.subs.push(watcher);
    }
    // Tell the observer to update
    notify () {
        console.log('Notify observer'.this.subs)
        this.subs.forEach( w= >w.update()); }}Copy the code

3. Implement a Watcher Watcher Observer as a bridge between the Observer and Compile.

A. Add yourself to the property observer (DEP) during self instantiation

B. Must have an update() method of its own

C, when dep.notify() is notified, it can call its own update() method and trigger the callback function cb bound in Compile

/ / observer
class Watcher {
    constructor (vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // Get the old value
        this.oldVal = this.getOldVal();
    }
    getOldVal () {
        Dep.target = this;
        const oldVal = compileUtil.getValue(this.expr, this.vm);
        Dep.target = null;
        return oldVal;
    }
    update () {
        // Determine whether the new value and the old value have changed
        const newVal = compileUtil.getValue(this.expr, this.vm);
        if(newVal ! = =this.oldVal) {
            this.cb(newVal)
        }
    }
}
Copy the code

Compile Compile mainly does things is to parse template instructions (such as V-HTML, V-text), replace variables in the template with data, and then initialize render page view, and bind the node corresponding to each instruction update function, add the observer to listen to the data, once the data changes, You are notified and the view is updated

class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // Get the document fragment object, put it in memory to reduce page backflow and redraw
        const fragment = this.node2Fragment(this.el);
        // console.log(fragment)

        // Compile the template
        this.compile(fragment);

        // Appends the child element to the root element
        this.el.appendChild(fragment);
    }
    compile (fragment) {
        // 1 Gets the child node
        const childNodes = fragment.childNodes;
        [...childNodes].forEach( child= > {
            // console.log(child);
            if (this.isElementNode(child)) {
                // is the element node
                // Compile the element node
                // console.log(' element node ', child)
                this.compileElement(child)
            }else {
                // Text node
                // console.log(' text node ', child)
                this.compileText(child)
            }

            if (child.childNodes && child.childNodes.length) {
                this.compile(child);
            }
        })
    }
    compileElement (node) {
        // <div v-text="msg"></div>
        const attributes = node.attributes;
        // console.log(attributes); 
        [...attributes].forEach(attr= > {
            const {name, value} = attr; // name=v-text | value = msg || name=@click value=handleClick
            // console.log(name);
            if (this.isDirective(name)) { // is the instruction v-text v-model v-html V-on :click
                const [, directive] = name.split("-"); // text, model, html, on:click
                const [dirName, eventName] = directive.split(":"); // dirName -> text, model, html, on
                // Update data data-driven view
                compileUtil[dirName](node, value, this.vm, eventName);
                // Remove attributes from tags with directives
                node.removeAttribute('v-' + directive)
            }else if(this.isEventDirective(name)){ // Check whether it is a listening event such as @
                const [, eventName] = name.split("@"); // eventDirective = click
                compileUtil['on'](node, value, this.vm, eventName);
                // Remove attributes from tags with directives
                node.removeAttribute(The '@' + eventName)
            }
        })
    }
    compileText (node) {
        / / compile {{}}
        const content = node.textContent;
        // console.log("content", content);
        if(/ \ {\ {(. +?) \} \} /.test(content)){
            // console.log(content);
            compileUtil['text'](node, content, this.vm); }}// Check if it is a Vue directive
    isDirective (attrName) {
        return attrName.startsWith("v-")}// Determine if the command is an event
    isEventDirective (attrName) {
        return attrName.startsWith("@")}// Check whether it is a node
    isElementNode (node) {
        return node.nodeType === 1;
    }
    node2Fragment (el) {
        // Create a document shard object
        const f = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            f.append(firstChild);
        }
        returnf; }}Copy the code

conclusion

Ok, so we probably shaped an interesting soul, here we mainly on the principle of bidirectional data binding, but for the template update is just a simple DOM operation, in fact in the Vue source code is through virtual VDOM + DIff algorithm combined with update queue to achieve, small partners interested in words, Can be viewed in depth Vue source code.

At this time, some students may have some interesting questions, such as:

1 Why is the publish-subscribe model so similar to the Observer model?

Here are the differences:

A In the observer mode, there are only two roles — observer + observed, and there is a third intermediary in the publish/subscribe mode

B The observer and the observed are loosely coupled. Publishers and subscribers, there is no coupling at all

2 How to view the complete code? Please jab here