Vue bidirectional data binding principle

Object.defineProperty

describe

Use to define a new property or modify an existing property on an object, and return the object

grammar

Object.defineProperty(obj, prop, descriptor)
Copy the code

parameter

  • Obj: An object on which attributes must be defined

  • Prop: The name or Symbol of the property that must be defined or modified

  • Descriptor: descriptor of a property to be defined or modified

    The details are as follows:

    interface Descriptor {
        // This property can be deleted or changed only if it is trueconfigurable? :boolean;
        // This property can be enumerated when true (using for... In or Object. The keys ())enumerable? :boolean;
        // The value corresponding to the property can be any type of value, default is undefinedvalue? :any;
        // The property value can be overridden only if it is truewritable? :boolean;
        Getters are a method of obtaining the value of an attributeget? () :any;
        // The setter is a method of setting the value of a propertyset? (v:any) :void;
    }
    Copy the code

    Some examples:

    // Examples of different signals
    let a = {
        p1: 10.p2: 'test'
    }
    Object.defineProperty(a, 'p1', {
        configurable: false.value: 20
    })
    delete a.p1 // Return false, this property cannot be deleted
    Copy the code
    // Enumerable example
    let b = {
        p1: 10.p2: 'test'
    }
    console.log(Object.keys(b)) Return ["p1", "p2"]
    Object.defineProperty(b, 'p1', {
        enumerable: false
    })
    console.log(Object.keys(b)) / / return/" p2"
    Copy the code
    // Example of writable
    let c = {
        p1: 10.p2: 'test'
    }
    Object.defineProperty(c, 'p1', {
        writable: false.value: 20
    })
    c.p1 / / 20
    c.p1 = 30
    console.log(c.p1) / / 20
    Copy the code
    // Examples of getter/setter
    // Note: The writable and value properties are not allowed when getters or setters are used
    let d = {
        p1: 10.p2: 'test'
    }
    Object.defineProperty(d, 'p1', {
        get: function () {
            // d.p1 returns 100 all the time
            return 100
        },
        set: function (newVal) {
            console.log(`new value:${newVal}`)}})console.log(d.p1)
    d.p1 = 25
    Copy the code

The return value

The object passed to the function. The first parameter, obj

compatibility

There will be compatibility issues below IE8

Implement a minimalist bidirectional binding

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
    	<input type="text" id="a">
    	<p id="b"></p>
    </div>
    <script>
        let obj = {}
        Object.defineProperty(obj, 'test', {
            set: function (newVal) {
                document.getElementById("a").value = newVal
                document.getElementById("b").innerHTML = newVal
            }
        })

        document.addEventListener('keyup'.function (e) {
            obj.test = e.target.value
        })
    </script>
</body>
</html>
Copy the code

MVVM

The MVVM mode divides the program into three parts: Model, View, and View-Model.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        {{ text }}
    </div>
    
    <script>
        const vm = new Vue({
            el: '#app'.data: {
                text: 'hello world'}})</script>
</body>
</html>
Copy the code

To achieve bidirectional data binding needs to be decomposed into three steps:

  1. Input box, text text and data in the data binding;
  2. View => model;
  3. Model => view; model => view;

DocumentFragment

describe

Document. CreateDocumentFragment (), create a new blank Document fragments (DocumentFragment).

DocumentFragment is a DOM node. They are not part of the main DOM tree. The usual use case is to create a document fragment, attach elements to the document fragment, and then attach the document fragment to the DOM tree. In the DOM tree, the document fragment is replaced by all of its child elements.

Because the document fragment exists in memory and not in the DOM tree, inserting child elements into the document fragment does not cause page backflow (a calculation of element location and geometry). Therefore, using document fragments generally results in better performance.

grammar

// Fragment refers to an empty DocumentFragment object
let fragment = document.createDocumentFragment();
Copy the code

The sample

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>

<body>
    <ul id="ul"></ul>
    <script>
        var element = document.getElementById('ul'); // assuming ul exists
        var fragment = document.createDocumentFragment();
        var browsers = ['Firefox'.'Chrome'.'Opera'.'Safari'.'Internet Explorer'];

        browsers.forEach(function (browser) {
            var li = document.createElement('li');
            li.textContent = browser;
            fragment.appendChild(li);
        });

        element.appendChild(fragment);
    </script>
</body>

</html>
Copy the code

Hijack child node

When Vue compiles, it hijacks all the children of the mount target (nodes in the DOM are automatically removed via the Append method) into the DocumentFragment, processes the DocumentFragment, and then returns the whole DocumentFragment to insert into the mount target.

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
    	<input type="text" id="a">
    	<p id="b"></p>
    </div>
    <script>
        const dom = nodeToFragment(document.getElementById('app'))
        console.log(dom)

        function nodeToFragment (node) {
            let fragment = document.createDocumentFragment()
            let child
            while (child = node.firstChild) {
                // The appendChild method has a subtle element that removes the child from the original DOM after the call
                // Hijack all child nodes of node
                fragment.appendChild(child)
            }
            return fragment
        }
    </script>
</body>
</html>
Copy the code

Data initialization binding

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="text">
        {{ text }}
    </div>
    <script>
        function nodeToFragment(node, vm) {
            let fragment = document.createDocumentFragment()
            let child

            while (child = node.firstChild) {
                compile(child, vm)
                fragment.appendChild(child)
            }
            return fragment
        }

        function compile(node, vm) {
            const reg = / \ {\ {(. *) \} \} /;
            // The node type is element
            if (node.nodeType === 1) {
                const attr = node.attributes;
                // Parse the attribute
                for (let i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName === 'v-model') {
                        let name = attr[i].nodeValue; // Get the name of the property bound to the V-Model
                        node.value = vm.data[name]; // Assign the data value to the node
                        node.removeAttribute('v-model')}}}// The node type is text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    let name = RegExp$1.// Get the matched string
                    name = name.trim()
                    node.nodeValue = vm.data[name]
                }
            }
        }

        function Vue(options) {
            this.data = options.data
            const id = options.el
            const dom = nodeToFragment(document.getElementById(id), this)

            // After compiling, return the DOM to the app
            document.getElementById(id).appendChild(dom)
        }

        const vm = new Vue({
            el: 'app'.data: {
                text: 'hello world'}})</script>
</body>

</html>

Copy the code

Reactive data binding

When we enter data into the input field, we first fire the Input event (or keyUp, change event). In the corresponding event handler, we take the value of the input field and assign it to the TEXT property of the VM instance. We’ll use defineProperty to set text in data to the VM’s getter property, so assigning a value to vm.text will trigger the set method. There are two main things to do in the set method, the first is to update the value of the property, and the second is reserved for task three.

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="text">
        {{ text }}
    </div>
    <script>
        function nodeToFragment(node, vm) {
            let fragment = document.createDocumentFragment()
            let child

            while (child = node.firstChild) {
                compile(child, vm)
                fragment.appendChild(child)
            }
            return fragment
        }

        function compile(node, vm) {
            const reg = / \ {\ {(. *) \} \} /;
            // The node type is element
            if (node.nodeType === 1) {
                const attr = node.attributes;
                // Parse the attribute
                for (let i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName === 'v-model') {
                        let name = attr[i].nodeValue; // Get the name of the property bound to the V-Model
                        node.addEventListener('input'.function (e) {
                            vm[name] = e.target.value
                        })
                        node.value = vm[name]; // Assign the data value to the node
                        node.removeAttribute('v-model')}}}// The node type is text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    let name = RegExp$1.// Get the matched string
                    name = name.trim()
                    node.nodeValue = vm[name]
                }
            }
        }

        function defineReactive(obj, key, val) {
            Object.defineProperty(obj, key, {
                get: function () {
                    return val
                },
                set: function (newVal) {
                    if (newVal === val) return
                    val = newVal;
                    console.log(val)
                }
            })
        }

        function observe(obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key])
            })
        }

        function Vue(options) {
            this.data = options.data
            const data = this.data
            observe(data, this)

            const id = options.el
            const dom = nodeToFragment(document.getElementById(id), this)

            // After compiling, return the DOM to the app
            document.getElementById(id).appendChild(dom)
        }

        const vm = new Vue({
            el: 'app'.data: {
                text: 'hello world'}})</script>
</body>

</html>
Copy the code

Subscribe&publish mode

The text property in the data changes, the set method fires, but the contents of the text node do not change. How do you synchronize text nodes that are also bound to text? Here’s another thing: the subscription publishing model.

The subscription publishing pattern (also known as the observer pattern) defines a one-to-many relationship in which multiple observers listen to a topic object at the same time, and all observers are notified when the topic object’s state changes.

The publisher sends a notification => The subject object receives the notification and pushes it to the subscriber => The subscriber performs the corresponding action

// Subscribers
const sub1 = { update: function () { console.log(1)}}const sub2 = { update: function () { console.log(2)}}const sub3 = { update: function () { console.log(3)}}// A theme
function Dep () {
    this.subs = [sub1, sub2, sub3]
}
Dep.prototype.notify = function () {
    this.subs.forEach(function (sub) {
        sub.update()
    })
}

/ / publisher
var pub = {
    publish: function () {
        dep.notify()
    }
}

var dep = new Dep()
pub.publish() / / 1 2 3
Copy the code

As mentioned earlier, the second thing you do when the set method fires is to send a notification as the publisher: “I am the property text and I have changed.” The text node acts as the subscriber and performs the corresponding update operations when the message is received.

const adadisPub = {
    adadisBook: [].// Adadis salesclerk's little book
    subShoe(phoneNumber) {       // The buyer is registered in the small book
        this.adadisBook.push(phoneNumber)
    },
    notify() {                     // The salesman calls the buyer of the notebook
        for (const customer of this.adadisBook) {
            customer.update()
        }
    }
}

const customer1 = {
    phoneNumber: '152xxx'.update() {
        console.log(this.phoneNumber + ': Go to the mall. ')}}const customer2 = {
    phoneNumber: '138yyy'.update() {
        console.log(this.phoneNumber + ': Buy a pair for my cousin ')
    }
}

adadisPub.subShoe(customer1)  // Leave the number in the small book
adadisPub.subShoe(customer2)

adadisPub.notify()            // Call the buyer to inform them of the arrival

// 152XXX: Go to the mall
// 138yyy: Buy a pair for my cousin
Copy the code

Implementation of bidirectional binding

We have implemented: Modify the input field content => Modify the property value in the event callback => trigger the property’s set method.

Dep.notify () => Triggers the subscriber’s update method => To update the view.

The key logic here is how to add a watcher to the DEP of the associated property.

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="text">
        {{ text }}
    </div>
    <script>
        function observe(obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key])
            })
        }

        function defineReactive(obj, key, val) {
            // When listening for data, a subject object deP is generated for each property in the data
            const dep = new Dep()
            Object.defineProperty(obj, key, {
                get: function () {
                    // Add subscriber watcher to the subject object Dep
                    if (Dep.target) {
                        dep.addSub(Dep.target)
                    }
                    return val
                },
                set: function (newVal) {
                    if (newVal === val) return
                    val = newVal
                    // Give a notice as the publisher
                    dep.notify()
                }
            })
        }

        function nodeToFragment(node, vm) {
            let fragment = document.createDocumentFragment()
            let child

            while (child = node.firstChild) {
                compile(child, vm)
                fragment.appendChild(child)
            }
            return fragment
        }

        function compile(node, vm) {
            const reg = / \ {\ {(. *) \} \} /;
            // The node type is element
            if (node.nodeType === 1) {
                const attr = node.attributes;
                // Parse the attribute
                let name
                for (let i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName === 'v-model') {
                        name = attr[i].nodeValue; // Get the name of the property bound to the V-Model
                        node.addEventListener('input'.function (e) {
                            vm[name] = e.target.value
                        })
                        node.value = vm[name]; // Assign the data value to the node
                        node.removeAttribute('v-model')}}// As the HTML is compiled, a subscriber Watcher is generated for each node associated with the data binding, and the watcher adds itself to the deP of the corresponding attribute
                new Watcher(vm, node, name, 'input');
            }
            // The node type is text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    let name = RegExp$1.// Get the matched string
                    name = name.trim()
                    new Watcher(vm, node, name, 'text'); }}}/ / subscribe
        function Watcher(vm, node, name, nodeType) {
            // First, we assign ourselves to a global variable dep.target
            Dep.target = this;
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.nodeType = nodeType;
            // The update method is executed, and the GET method is executed. The GET method reads the VM's accessor properties, triggering the GET method, which adds the watcher to the DEP of the corresponding accessor property
            this.update();
            Dep.target = null;
        }
        Watcher.prototype = {
            update: function () {
                this.get();
                // Update the view
                if (this.nodeType === 'text') {
                    this.node.nodeValue = this.value;
                }
                if (this.nodeType === 'input') {
                    this.node.value = this.value; }},// Get the attribute value in data
            get: function () {
                // Get the value of the attribute
                this.value = this.vm[this.name]; // Trigger get for the corresponding property}}/ / theme
        function Dep() {
            this.subs = []
        }
        Dep.prototype = {
            addSub: function (sub) {
                this.subs.push(sub);
            },
            notify: function () {
                this.subs.forEach(function (sub) { sub.update(); }); }}function Vue(options) {
            this.data = options.data
            const data = this.data
            observe(data, this)
            const id = options.el
            const dom = nodeToFragment(document.getElementById(id), this)
            // After compiling, return the DOM to the app
            document.getElementById(id).appendChild(dom)
        }

        const vm = new Vue({
            el: 'app'.data: {
                text: 'hello world'}})</script>
</body>

</html>
Copy the code