How is the Vue2. X response formula principle implemented?

One of the most unique features of Vue is its non-invasive responsive system. So what is the reactive principle?

The data model is just a normal JavaScript object, and as we modify the data, the view is updated, eliminating tedious DOM manipulation and improving development efficiency. In short, as the data changes, the view updates.

Once you know the concept, how does it work?

You use the getter and setter methods in Object.defineProperty() and the observer pattern in the design pattern.

So, let’s look at Object.defineProperty() first. In MDN it is explained like this: the object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.

let data = {
	msg:'hello'
};

let vm = {};

Object.defineProperty(vm, 'msg', {
        enumerable: true.// enumerable (traversable)
        configurable: true.// configurable (can be deleted using delete, can be redefined via defineProperty)

        // Execute when the value is fetched
        get() {
            console.log('get', data.msg);
            return data.msg
        },
        // Execute when set value
        set(newVal) {
            if (newVal === data.msg) {
                return
            }
            data.msg = newVal;
            console.log('set', data.msg); }})/ / test

console.log(vm.msg);
/* > "get" "hello" > "hello" */
vm.msg = 'world'; // > "set" "world"

Copy the code

After a brief introduction to Object.defineProperty(), there’s the observer pattern, which you might think of as publish-subscribe. In fact, their essence is the same, but there are certain differences.

Let’s start with the publish-subscribe model.

The publisher-subscriber model consists of three modules, publisher, subscriber and unified dispatch center. This unified dispatch center is the equivalent of a newspaper office. The publisher is like a magazine owner who comes to the center to register a magazine, and the subscriber is like a user, and I subscribe to the magazine at the center. Whenever a publisher releases an issue of the magazine, subscribers are notified by the clerk’s office to pick up the new issue. The publish-subscriber pattern is invoked by a unified dispatch center, so publishers and subscribers do not need to know of each other’s existence.

Below, we’ll take a closer look at the publish-subscribe pattern with an example of implementing Vue custom events.

function EventEmitter(){
    // Initialize the unified scheduling center
    this.subs = Object.create(null); // {'click':[fn1,fn2]}
}
// Register events
EventEmitter.prototype.$on = function (eventType,handler){
        console.log(this);
        this.subs[eventType]= this.subs[eventType]||[];
        this.subs[eventType].push(handler);
}
// Trigger the event
EventEmitter.prototype.$emit = function (eventType,data){
        if(this.subs[eventType]){
                this.subs[eventType].forEach(handler= >{ handler(data); }); }}/ / test
const em = new EventEmitter();

/ / subscriber
em.$on('click1'.(data) = >{
    console.log(data);
})
/ / publisher
em.$emit('click1'.'maomin') //maomin
Copy the code

This custom event is widely used for Vue peer component transfer.

Next, let’s introduce the observer pattern.

The observer pattern is scheduled by the target. For example, when an event is triggered, the target invokes the observer’s methods, so there is a dependency between the observer’s subscriber (the observer) and the publisher (the target).

// Publisher (target)
function Dep(){
    this.subs = [];
}
Dep.prototype.addSub = function (sub){
    if(sub&&sub.update){
            this.subs.push(sub);
    }
}

Dep.prototype.notify = function (data){
        this.subs.forEach(sub= >{ sub.update(data); })}// Subscriber (observer)
function Watcher(){}
    Watcher.prototype.update=function(data){
    console.log(data);
}

/ / test
let dep = new Dep();
let watcher = new Watcher();
// Collect dependencies
dep.addSub(watcher);
// Send notifications
dep.notify('1');
dep.notify('2');
Copy the code

The following figure distinguishes the two modes.

Implement the Vue2. X mini version

The purpose of implementing a Vue mini-version is to deepen your understanding of the Vue responsive principle and some of its apis. First of all, we analyze the overall structure of Vue2. X response formula principle.

As shown below:

Next, we will implement a mini Vue based on the process described in this picture. Vue2. X uses the Virtual DOM, but since we only need to implement a mini-version here, we have simplified it by manipulating the DOM directly.

Now, let’s see how I built a Vue Mini.

The first step

The page structure is as follows, we can first introduce the full version of Vue2. X, see the implementation effect.

<! 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>Vue2.x Reactive</title>
</head>

<body>
    <div id="app">
        <h2>Text node</h2>
        <div>{{msg}}</div>
        <div>{{count}}</div>
        <div>{{obj.name}}</div>
        <div>{{arr[0]}}</div>
        <div>{{obj.inner.age}}</div>
        <div>{{obj.inner.name}}</div>
        <h2>v-text</h2>
        <div v-text="msg"></div>
        <h2>v-model</h2>
        <input type="text" v-model="msg">
        <input type="text" v-model="count">
        <h2>v-html</h2>
        <div v-html="html"></div>
        <h2>v-show</h2>
        <div v-show="isShow">{{isShow}}</div>
        <h2>v-on</h2>
        <button v-on:click="handler">handler</button>
        <button @click="onClick">onClick</button>
        <h2>v-if</h2>
        <div>
            <p v-if="isIf">{{isIf}}</p>
        </div>
    </div>
    <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
    <script>
        const vm = new Vue({
                el: '#app'.data() {
                    return {
                        msg: 'maomin'.count: 1.obj: {
                            name: 'hello'.inner: {
                                age: 17}},arr: ['string1'].html: '<div>{{msg}}</div>'.isShow: false.isIf:true}},methods: {
                    handler() {
                        // this.count = 2;
                        this.isIf = !this.isIf;
                    },
                    onClick() {
                        this.obj.inner.age = 18;
                        // console.log(this.obj.inner.age);}}});</script>
</body>

</html>
Copy the code

After testing, the full version of Vue2. X looks like this. We will use the Vue mini version to achieve the same page effect.

The second step

We will build the Vue mini-version, which we will call vuemini.js, based on the overall structure diagram and page structure.

Through the overall structure diagram, we can find that there are several constructors including Vue, Observer, Compiler, Dep and Watcher. We’ll start by creating these constructors, which are not defined using the class class because most of the Vue source code also uses constructors, and are relatively extensible.

Vue

/ / instance.
function Vue(options) {
    this.$options = options || {};
    this._data = typeof options.data === 'function' ? options.data() : options.data || {};
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
    // Is responsible for injecting properties from data into Vue instances and converting them into getters/setters
    this._proxyData(this._data);
    this.initMethods(this, options.methods || {})
    // Responsible for calling observer to listen for changes to all properties of data
    new Observer(this._data);
    // Is responsible for calling compiler parsing instructions/interpolation expressions
    new Compiler(this);
}
// Mount properties from data to this
Vue.prototype._proxyData = function (data) {
    Object.keys(data).forEach(key= > {
        Object.defineProperty(this, key, {
            configurable: true.enumerable: true.get() {
                return data[key]
            },
            set(newVal) {
                if (newVal === data[key]) {
                    return} data[key] = newVal; }})})}function noop(a, b, c) {}function polyfillBind(fn, ctx) {
    function boundFn(a) {
        var l = arguments.length;
        return l
            ? l > 1
                ? fn.apply(ctx, arguments)
                : fn.call(ctx, a)
            : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
}
function nativeBind(fn, ctx) {
    return fn.bind(ctx)
}
const bind = Function.prototype.bind
    ? nativeBind
    : polyfillBind;

// Initialize the methods attribute
Vue.prototype.initMethods = function (vm, methods) {
    for (var key in methods) {
        {
            if (typeofmethods[key] ! = ='function') {
                warn(
                    "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
                    "Did you reference the function correctly?",
                    vm
                );
            }
        }
        vm[key] = typeofmethods[key] ! = ='function'? noop : bind(methods[key], vm); }}Copy the code

Observer

// Data hijacking.
// Convert attributes in the data (_data) option to responsive data.
function Observer(data) {
    this.walk(data);
}
Observer.prototype.walk = function (data) {
    if(! data ||typeofdata ! = ='object') {
        return
    }
    Object.keys(data).forEach(key= > {
        this.defineReactive(data, key, data[key]);
    })
}
Observer.prototype.defineReactive = function (obj, key, val) {
    let that = this;
    // Collect dependencies
    let dep = new Dep();
    // If val is an object, convert the attributes inside val to responsive data
    this.walk(val);
    Object.defineProperty(obj, key, {
        enumerable: true.configurable: true.get() {
            // Collect dependencies
            Dep.target && dep.addSub(Dep.target)
            return val
        },
        set(newVal) {
            if (newVal === val) {
                return
            }
            val = newVal;
            // Reassign attributes in data to convert them to responsive data.
            that.walk(newVal);
            // Send notificationsdep.notify(); }})}Copy the code

Compiler

// Compile templates, parse instructions/interpolation expressions
// Be responsible for the first rendering of the page
// Re-render the view when the data changes
function Compiler(vm) {
    this.el = vm.$el;
    this.vm = vm;
    // Compile the template immediately
    this.compile(this.el);
}
// Compile templates to handle text nodes and element nodes
Compiler.prototype.compile = function (el) {
    let childNodes = el.childNodes;
    Array.from(childNodes).forEach(node= > {
        // Process text nodes
        if (this.isTextNode(node)) {
            this.compileText(node);
        }
        // Process element nodes
        else if (this.isElementNode(node)) {
            this.compileElement(node);
        }
        // Determine whether a node has children. If there are children, recursively call compile
        if (node.childNodes && node.childNodes.length) {
            this.compile(node); }})}// Compile text nodes to handle interpolation expressions
Compiler.prototype.compileText = function (node) {
    // console.dir(node);
    let reg = / \ {\ {(. +?) \} \} /;
    let value = node.textContent;
    if (reg.test(value)) {
        let key = RegExp.$1.trim();
        if (this.vm.hasOwnProperty(key)) {
            node.textContent = value.replace(reg, typeof this.vm[key] === 'object' ? JSON.stringify(this.vm[key]) : this.vm[key]);
            // Create a Watcher object to update the view when data changes
            new Watcher(this.vm, key, (newVal) = >{ node.textContent = newVal; })}else {
            const str = `this.vm.${key}`;
            node.textContent = value.replace(reg, eval(str));
            // Create a Watcher object to update the view when data changes
            new Watcher(this.vm, key, () = > {
                const strw = `this.vm.${key}`;
                node.textContent = value.replace(reg, eval(strw)); })}}}// Determine whether the node is a text node
Compiler.prototype.isTextNode = function (node) {
    return node.nodeType === 3;
}
// Determine if the node is an element node
Compiler.prototype.isElementNode = function (node) {
    return node.nodeType === 1;
}

// Compile element node, process instruction
Compiler.prototype.compileElement = function (node) {
    // console.log(node.attributes);

    // Iterate over all attribute nodes
    Array.from(node.attributes).forEach(attr= > {
        let attrName = attr.name;
        // console.log(attrName);
        // Check if it is a command
        if (this.isDirective(attrName)) {
            // Judge: such as V-on :click
            let eventName;
            if (attrName.indexOf(':')! = = -1) {
                const strArr = attrName.substr(2).split(':');
                attrName = strArr[0];
                eventName = strArr[1];
            } else if (attrName.indexOf(The '@')! = = -1) {
                eventName = attrName.substr(1);
                attrName = 'on';
            } else {
                attrName = attrName.substr(2);
            }
            let key = attr.value;

            this.update(node, key, attrName, eventName); }})}// Determine if element attributes are directives
Compiler.prototype.isDirective = function (attrName) {
    return attrName.startsWith('v-') || attrName.startsWith(The '@');
}
// Instruction auxiliary function
Compiler.prototype.update = function (node, key, attrName, eventName) {
    let updateFn = this[attrName + 'Updater'];
    updateFn && updateFn.call(this, node, this.vm[key], key, eventName);
}
// Process the V-text instruction
Compiler.prototype.textUpdater = function (node, value, key) {
    node.textContent = value;
    new Watcher(this.vm, key, (newVal) = >{ node.textContent = newVal; })}// Handle v-HTML directives
Compiler.prototype.htmlUpdater = function (node, value, key) {
    node.insertAdjacentHTML('beforeend', value);
    new Watcher(this.vm, key, (newVal) = > {
        node.insertAdjacentHTML('beforeend', newVal); })}// Handle the V-show command
Compiler.prototype.showUpdater = function (node, value, key) {
    !value ? node.style.display = 'none' : node.style.display = 'block'
    new Watcher(this.vm, key, (newVal) = > {
        !newVal ? node.style.display = 'none' : node.style.display = 'block'; })}// Process the V-if directive
Compiler.prototype.ifUpdater = function (node, value, key) {
    const nodew = node;
    const nodep = node.parentNode;
    if(! value) { node.parentNode.removeChild(node) }new Watcher(this.vm, key, (newVal) = > {
        console.log(newVal);
        !newVal ? nodep.removeChild(node) : nodep.appendChild(nodew);
    })
}
// Process the V-ON instruction
Compiler.prototype.onUpdater = function (node, value, key, eventName) {
    if (eventName) {
        const handler = this.vm.$options.methods[key].bind(this.vm); node.addEventListener(eventName, handler); }}// Process the V-model directive
Compiler.prototype.modelUpdater = function (node, value, key) {
    node.value = value;
    new Watcher(this.vm, key, (newVal) = > {
        node.value = newVal;
    })
    // Bidirectional binding, view changes update data
    node.addEventListener('input'.() = > {
        this.vm[key] = node.value; })}Copy the code

Dep

// Publisher.
// Collect dependencies and add all watcher. Notify all observers.
function Dep() {
    // Store all watcher
    this.subs = [];
}
// Add an observer
Dep.prototype.addSub = function (sub) {
    if (sub && sub.update) {
        this.subs.push(sub); }}// Send notifications
Dep.prototype.notify = function () {
    this.subs.forEach(sub= >{ sub.update(); })}Copy the code

Watcher

function Watcher(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // Record the current watcher object to the Dep class's static property target
    Dep.target = this;
    if (vm.hasOwnProperty(key)) {
        this.oldVal = vm[key];
    } else {
        const str = `vm.${key}`;
        this.oldVal = eval(str);
    }
    Dep.target = null;
}
// Update the view when data changes
Watcher.prototype.update = function () {
    let newVal;
    if (this.vm.hasOwnProperty(this.key)) {
        newVal = this.vm[this.key];
    } else {
        const str = `this.vm.The ${this.key}`;
        newVal = eval(str);
    }
    this.cb(newVal);
}
Copy the code

These constructors implement what we call mini-versions, combining them into a single file vuemini.js. In the above prompts page introduction, see the effect.

In addition, I bound an HTML property in data with a value of ‘

{{MSG}}

‘, and the Maomin text below the V-HTML is rendered as compared to the previous full version.

Mini version of Vue2. X developed by UVU

Next, we’ll take a look at the mini version developed by THE University of Utah, which introduces the Virtual DOM, but is primarily responsive to the principle. We can compare the mini version with the above version to see if there are any similarities.

<! 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>vue2mini</title>
</head>

<body>
    <div id="app"></div>
    <script>
        // reactivity ---
        let activeEffect

        class Dep {
            subscribers = new Set(a)depend() {
                if (activeEffect) {
                    this.subscribers.add(activeEffect)
                }
            }
            notify() {
                this.subscribers.forEach(effect= > effect())
            }
        }

        function watchEffect(effect) {
            activeEffect = effect
            effect()
            activeEffect = null
        }

        function reactive(raw) {
            // use Object.defineProperty
            // 1. iterate over the existing keys
            Object.keys(raw).forEach(key= > {
                // 2. for each key: create a corresponding dep
                const dep = new Dep()

                // 3. rewrite the property into getter/setter
                let realValue = raw[key]
                Object.defineProperty(raw, key, {
                    get() {
                        // 4. call dep methods inside getter/setter
                        dep.depend()
                        return realValue
                    },
                    set(newValue) {
                        realValue = newValue
                        dep.notify()
                    }
                })
            })
            return raw
        }

        // vdom ---
        function h(tag, props, children) {
            return { tag, props, children };
        }

        function mount(vnode, container, anchor) {
            const el = document.createElement(vnode.tag);
            vnode.el = el;
            // props
            if (vnode.props) {
                for (const key in vnode.props) {
                    if (key.startsWith('on')) {
                        el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
                    } else{ el.setAttribute(key, vnode.props[key]); }}}if (vnode.children) {
                if (typeof vnode.children === "string") {
                    el.textContent = vnode.children;
                } else {
                    vnode.children.forEach(child= >{ mount(child, el); }); }}if (anchor) {
                container.insertBefore(el, anchor)
            } else{ container.appendChild(el); }}function patch(n1, n2) {
            // Implement this
            // 1. check if n1 and n2 are of the same type
            if(n1.tag ! == n2.tag) {// 2. if not, replace
                const parent = n1.el.parentNode
                const anchor = n1.el.nextSibling
                parent.removeChild(n1.el)
                mount(n2, parent, anchor)
                return
            }

            const el = n2.el = n1.el

            // 3. if yes
            / / 3.1 diff props
            const oldProps = n1.props || {}
            const newProps = n2.props || {}
            for (const key in newProps) {
                const newValue = newProps[key]
                const oldValue = oldProps[key]
                if(newValue ! == oldValue) {if(newValue ! =null) {
                        el.setAttribute(key, newValue)
                    } else {
                        el.removeAttribute(key)
                    }
                }
            }
            for (const key in oldProps) {
                if(! (keyin newProps)) {
                    el.removeAttribute(key)
                }
            }
            / / diff 3.2 children
            const oc = n1.children
            const nc = n2.children
            if (typeof nc === 'string') {
                if(nc ! == oc) { el.textContent = nc } }else if (Array.isArray(nc)) {
                if (Array.isArray(oc)) {
                    // array diff
                    const commonLength = Math.min(oc.length, nc.length)
                    for (let i = 0; i < commonLength; i++) {
                        patch(oc[i], nc[i])
                    }
                    if (nc.length > oc.length) {
                        nc.slice(oc.length).forEach(c= > mount(c, el))
                    } else if (oc.length > nc.length) {
                        oc.slice(nc.length).forEach(c= > {
                            el.removeChild(c.el)
                        })
                    }
                } else {
                    el.innerHTML = ' '
                    nc.forEach(c= > mount(c, el))
                }
            }
        }

        // paste all previous code from Codepen
        const app = {
            data: reactive({
                count: 0
            }),
            render() {
                return h('div', {
                    onClick: () = > {
                        app.data.count++
                    }
                }, String(app.data.count))
            }
        }

        function mountApp(component, selector) {
            let isMounted = false
            let oldTree
            watchEffect(() = > {
                if(! isMounted) { mount(oldTree = component.render(),document.querySelector(selector))
                    isMounted = true
                } else {
                    const newTree = component.render()
                    patch(oldTree, newTree)
                    oldTree = newTree
                }
            })
        }

        mountApp(app, '#app')
    </script>
</body>

</html>
Copy the code

About the author

Author: Vam’s Golden Bean Road. CSDN blog star of the Year 2019, CSDN blog has reached millions of visitors. Nuggets blog post repeatedly pushed to the home page, the total page view has reached hundreds of thousands.

In addition, my public number: front-end experience robbed road, the public continues to update the latest front-end technology and related technical articles. Welcome to pay attention to my public number, let us together in front of the road experience rob it! Go!