[Don't come to me after reading this] Explain the Vue data two-way binding principle

Posted by hoogie on Thu, 25 Nov 2021 18:05:48 +0100

Preface

After sorting out my own understanding of the principle of two-way binding of Vue data after learning, I by the way exclamate that Yuyuxi is too strong. If there are any errors, you are welcome to correct them.

1. Review of Basic Knowledge

1.1 Get attribute values chained using reduce method

If the reduce method is not yet used by the little buddy, go to review it!

First declare an object for us to use:

let obj = {
    name: 'zs',
    info: {
        age: 19,
        address: {
            location: 'Xi'an'
        }
    }
}

Interpolation expressions are often encountered in Vue, such as {{xxx}}

So now there's a requirement to give you a string:'obj.info.address.location'so that you can get the value of the location property, you can use the reduce method of the array to get it chain-wise:

//Given string
const attrStr = 'info.address.location'

//First split the given string into an array of attributes using the split method
//Then the chain value can be obtained using the array reduce method
const location = attrStr.split('.').reduce((newObj,key) => newObj[key],obj)

console.log(location);//Xi'an

1.2 Publish-Subscribe Mode

Publish-Subscribe is the core idea of Vue to implement data binding in both directions, so what is publish-subscribe mode:

1.2.1 Dep class
  • Responsible for Dependency Collection
  • There is an array inside that holds all the subscription information
  • Second, provide a way to append subscription information to the array
  • Finally, provide a way to iterate through each subscription in the array
1.2.2 Watcher class
  • Responsible for subscribing to events
1.2.3 Create the simplest publish-subscription

Create Dep class:

// Dep Class Collection Dependency
class Dep{
	//Constructor
    constructor(){
        // Array of methods to hold all subscribers
        this.subs = []  
    }
    // Ways to add subscribers
    addSub(watcher) {
        this.subs.push(watcher)
    }

    // Method of publishing subscriptions
    notify() {
        this.subs.forEach(watcher => {
        	//Calling the update method of the Watcher instance is equivalent to notifying it to update its dom
            watcher.update()
        })
    }
}

Create a Watcher class:

// Subscriber
class Watcher{
	//Here, the callback function passed in when cb created the instance defines some operations to update the dom
    constructor(cb) {
    	//Mount the incoming callback function cb onto the generated Watcher instance
        this.cb = cb
    }

    update() {
        this.cb()
    }
}

Next, let's create a few Watcher instances to test them:

// Instantiate two Watcher s
//The incoming callback function should have been some operation to update the dom, so let's just replace it with an output first
const w1 = new Watcher(() => {
    console.log('watcher1');
})
const w2 = new Watcher(() => {
    console.log('watcher2');
})

Create a Dep instance and add subscription information:

const dep = new Dep()

// Add Subscriber Information
dep.addSub(w1)
dep.addSub(w2)

// This operation will be monitored as long as we update the data in vue
// Then vue sends the change to the subscriber
dep.notify()//Output watcher1 and watcher2

Run result:
PS: The method of updating the dom will be executed only if dep.notify() is called to represent a notification being issued (two outputs instead here)

Summarize how publish-subscribe works:

  • First, each dom element is actually an instance of the Watcher class, and the Dep class acts as a link.
  • Dependent Collection: At the moment the dom structure is created, the corresponding watcher is added to the array in Dep, which is equivalent to a person giving Dep his own way of contact.
  • Publish a subscription: When we update the data in the Vue, the operation is monitored, and the Vue sends the change to the subscriber, that is, by just contacting, notifying the corresponding watcher of the update;
    In the code above, the update method in the Watcher instance is equivalent to how the instance is contacted, and Dep notify notifies subscribers by calling update through the notify method.

1.3 Role of Object.defineProperty:

Two methods of get() and set() are introduced.

First, provide an object:

const obj = {
    name: 'zs',
    age: 20,
    info: {
        a:1,
        c:2
    }
}

Using Object.defineProperty:

Object.defineProperty(obj,'name',{
	// Is it allowed to be looped
    enumerable: true,
    // Is configuration allowed
    configurable: true,
    
    // Triggered when the corresponding property is acquired
    get () {
        console.log('Acquired obj.name Value of');
        return 'I am not zs'
    },
    // Triggered when the corresponding property is modified/updated
    set (newVal) {
        console.log('Someone assigned me a value',newVal);
    }
})

console.log(obj.name);

obj.name = 'ls'

2. Start to manually bind Vue data in both directions

2.1 Manual data hijacking

To achieve data hijacking, add get and set methods to the data

2.1.1 Create an html and introduce your own vue.js
<!DOCTYPE html>
<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">
        <h3>Name is: {{name}}</h3>
        <h3>Age is:{{ age }}</h3>
        <h3>info.a The value is:{{info.a}}</h3>
        <div>name Value of:<input type="text" v-model="name"></div>

        <div>info.a Value of:<input type="text" v-model="info.a"></div>

    </div>


    <script src="./vue.js"></script>
    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                name: 'zs',
                age: 20,
                info: {
                    a: 'a1',
                    c: 'c1'
                }
            }
        })

        console.log(vm);
    </script>
</body>

</html>
2.1.2 Implement your own data hijacking in vue.js:
//First create the Vue class
class Vue {
    constructor(options) {
        //Mount Data
        this.$data = options.data
        // Call the method of data hijacking to transfer the data to be hijacked
        Observe(this.$data)
    }
}


2.1.3 Defines a method for data hijacking:

Add get and set methods for attributes in obj:

function Observe(obj) {
    // Get each property on obj
    Object.keys(obj).forEach(key => {
    
        // Add get and set for the current key corresponding property
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {},
            set() {}
        })
    })

}

Note: info in the data is also an object at this time, so are its properties added to get and set methods?
The answer is no!!! Because froEach only loops through the outermost objects

2.1.4 Use recursion as the underlying object property to also bind get and set
// Define a method for data hijacking
function Observe(obj) {

    // Recursive end condition
    if (!obj || typeof obj !== 'object') {
        return
    }

    // const dep = new Dep()

    // Get each property on obj
    Object.keys(obj).forEach(key => {
        //	Add getter s and setter s for the current key property
        //	Property value corresponding to key currently being looped
        let value = obj[key]
        // value is equivalent to an attribute child node, so recurse here
        Observe(value)

        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                console.log(`Someone got it ${key}Value of`);
                return value
            },
            set(newVal) {
                value = newVal
            }
        })
    })

}

At this point, the inner properties also have get and set methods:

Then think again, if you assign a new attribute, will it have get and set methods?
The answer is definitely no, because properties that previously added get and set have been overridden. The next step is to solve the problem:

2.1.5 also automatically adds get and set methods for newly added attributes:

Now that a reassignment has been made, the set method will be triggered

// Define a method for data hijacking
function Observe(obj) {

    // Recursive end condition
    if (!obj || typeof obj !== 'object') {
        return
    }

    // const dep = new Dep()

    // Get each property on obj
    Object.keys(obj).forEach(key => {
        //	Add getter s and setter s for the current key property
        //	Property value corresponding to key currently being looped
        let value = obj[key]
        // value is equivalent to an attribute child node, so recurse here
        Observe(value)

        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                console.log(`Someone got it ${key}Value of`);
                return value
            },
            set(newVal) {
                value = newVal
                //Attribute hijacking also occurs for newly assigned attributes
                Observe(value)
            }
        })
    })

}

2.2 Attribute proxy for later operation

In fact, this step does not affect the two-way binding at all. It is equivalent to adding an agent for easy operation.

class Vue {
    constructor(options) {
        //Mount Data
        this.$data = options.data
        // Method to invoke data hijacking
        Observe(this.$data)
        // Proxy Attributes
        Object.keys(this.$data).forEach((key) => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newVal) {
                    this.$data[key] = newVal
                }
            })
        })
    }
}

2.3 Document Fragmentation

When the dom structure is rendered on the page, but each time the data is assigned a value, the page will need to be redrawn or rearranged. Because the cost of manipulating the dom is very "expensive", there is the concept of document fragmentation. dom structure is put into memory, filled with data before rendering. So-called document fragmentation, can be said to be a piece of memory.

2.3.1 Manual simulation of template compilation methods

Function Definition:

// Method of template compilation for html structure
function Compile(el, vm) {
    // Get the corresponding dom structure of el and mount it on vm
    vm.$el = document.querySelector(el)

    // Create document fragmentation to improve the performance of dom operations
    const fragment = document.createDocumentFragment()
    //Using loops, put the dom structure into document fragmentation
    while (childNode = vm.$el.firstChild) {
        fragment.appendChild(childNode)
    }

    // Compile Templates
    replace(fragment)
    
	//	Render document fragments when compilation is complete
    vm.$el.appendChild(fragment)



    // Internally defined method responsible for compiling the dom template
    function replace(node) {
        // Regular matching of interpolation expressions
        const regMusache = /\{\{\s*(\S+)\s*\}\}/

        // Proves that the current node is a text child node and requires regular replacement
        if (node.nodeType === 3) {
            // Note: Text subnode is also a dom object
            const text = node.textContent

            // Perform regular string matching and extraction
            const execResult = regMusache.exec(text)
            // If the match rule succeeds, proceed to the next step
            if (execResult) {
            	//Chain to get the corresponding attribute value 
                const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
				// Replace the corresponding part of the interpolation expression with value
                node.textContent = text.replace(regMusache, value)
            // Terminate recursion
            return
        }
        // The proof is not a text node and needs to be processed recursively
        node.childNodes.forEach(child => {
            replace(child)
        })
    }
}

Function call:

class Vue {
    constructor(options) {
     //-------------------------- Omit the above operation------------------------------------------------------------------------------------------------------------------------
     
        // Call template compiled functions
        Compile(options.el, this)
    }
}
2.3.2 Collecting dependencies of all eligible nodes during template compilation

If you don't add a publish-subscription, the page can only be replaced with a value in the moment it opens, and it can't bind data in both directions, so you need to put the operation to update the dom in the callback function cb of the watcher instance

First create the Dep class (collection dependencies):

// Class for collecting subscribers
class Dep {
    constructor() {
        this.subs = []
    }

    addSub(watcher) {
        this.subs.push(watcher)
    }

    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}

Create a Watcher class:

// Subscriber's class
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb
    }

    update() {
        // Get the latest value and pass it to the callback function
        const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)

        this.cb(value)
    }
}

Add a watcher class to the replace function, which collects the dependencies of eligible nodes and tracks it:

// The method responsible for compiling the dom template
    function replace(node) {
       //------------------- Omit-----------------
        if (node.nodeType === 3) {
            	//--------------------- Other operations see the previous step-----------------------
                
                // Create a Watcher instance at this time
                new Watcher(vm, execResult[1], (newValue) => {
                    // Operation to update the dom
                    node.textContent = text.replace(regMusache, newValue)
                })
            }

            // Terminate recursion
            return
        }
       //--------------------- Other operations see the previous step-----------------------
    }

2.4 Add Watcher class to Dep class and bind data->view

So how did he implement adding the Watcher class to the Dep class?

The next step is to design the most clever three lines of magic code!!!:

// Subscriber's class
class Watcher {
    constructor(vm, key, cb) {
        .......

        // The following three lines of magic code are responsible for saving the Watcher instance created to the subs of the Dep instance
        Dep.target = this
        key.split('.').reduce((newObj, k) => newObj[k], vm)
        Dep.target = null
    }

    update() {
        ......
    }
}

How do you understand?

  • Dep.target = this, this time this is pointing to the Watcher instance we just created, defining a new attribute on the Dep class, target, saving its value
  • Key.split ('.').reduce ((newObj, k) => newObj[k], vm), this step is to get the value of the property monitored by this instance, then since it will trigger Observe r's get method, we collect the dependent operations in the get method
  • Dep.target = null collects the dependencies of this watcher instance, that is, after adding it to Dep, empty the target for the next round of operations

Implement collection of watcher instance dependencies in Observe r's get method:

			get() {
                //As long as the target is not empty, the watcher instance you just created is placed in subs
                Dep.target && dep.addSub(Dep.target)
                console.log(`Someone got it ${key}Value of`);
                return value
            },

Finally, when we modify the data in the data, the set method of Observe r is triggered, so we define the publish subscription operation in the set, which is to call the notify method of dep:

			set(newVal) {
                value = newVal
                Observe(value)
                // Notify each subscriber to update their own content
                dep.notify()
            }

In the update method, get the latest value and pass it to cb:

	update() {
        // Get the latest value and pass it to the callback function
        const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)

        this.cb(value)
    }

2.5 Implement View->Data Binding

In the replace method, when the read dom node is an input, operate:

// Prove that the current node is an input box
	if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
            const attrs = Array.from(node.attributes)
            const findResult = attrs.find(x => x.name === 'v-model')

            // Explanation has the attribute v-model
            if (findResult) {
                // Get the current v-model value
                const expStr = findResult.value

                const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
                // However, if you assign the value this way directly, the value in the Re-modify Value text box will not change. You need to add a Watcher instance to turn on publish-subscribe mode
                node.value = value

                // Add watcher instance
                new Watcher(vm, expStr, (value) => {
                    node.value = value
                })


                // Listen for input input events to get the latest values and update the latest values to vm
                node.addEventListener('input',(e) => {
                    const keyArr = expStr.split('.')
                    const obj = keyArr.slice(0,keyArr.length-1).reduce((newObj,k) => newObj[k],vm)
                    obj[keyArr[keyArr.length-1]] = e.target.value
                })

            }
        }

3. Complete code and process

html:

<!DOCTYPE html>
<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">
        <h3>Name is: {{name}}</h3>
        <h3>Age is:{{ age }}</h3>
        <h3>info.a The value is:{{info.a}}</h3>
        <div>name Value of:<input type="text" v-model="name"></div>

        <div>info.a Value of:<input type="text" v-model="info.a"></div>

    </div>


    <script src="./vue.js"></script>
    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                name: 'zs',
                age: 20,
                info: {
                    a: 'a1',
                    c: 'c1'
                }
            }
        })

        console.log(vm);
    </script>
</body>

</html>

vue.js:

class Vue {
    constructor(options) {
        //Mount Data
        this.$data = options.data
        // Method to invoke data hijacking
        Observe(this.$data)

        // Property Agent
        Object.keys(this.$data).forEach((key) => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newVal) {
                    this.$data[key] = newVal
                }
            })
        })

        // Call template compiled functions
        Compile(options.el, this)

    }


}

// Define a method for data hijacking
function Observe(obj) {

    // Recursive end condition
    if (!obj || typeof obj !== 'object') {
        return
    }

    const dep = new Dep()

    // Get each property on obj
    Object.keys(obj).forEach(key => {
        // Add getter s and setter s for the current key property

        //Property value corresponding to key currently being looped
        let value = obj[key]
        // value is equivalent to an attribute child node, so recurse here
        Observe(value)

        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                //As long as the target is not empty, the watcher instance you just created is placed in subs
                Dep.target && dep.addSub(Dep.target)
                console.log(`Someone got it ${key}Value of`);
                return value
            },
            set(newVal) {
                value = newVal
                Observe(value)
                // Notify each subscriber to update their own content
                dep.notify()
            }
        })
    })

}

// Method of template compilation for html structure
function Compile(el, vm) {
    // Get the corresponding dom structure of el
    vm.$el = document.querySelector(el)

    // Create document fragmentation to improve the performance of dom operations
    const fragment = document.createDocumentFragment()
    while (childNode = vm.$el.firstChild) {
        fragment.appendChild(childNode)
    }


    // Compile Templates
    replace(fragment)

    vm.$el.appendChild(fragment)



    // The method responsible for compiling the dom template
    function replace(node) {
        // Regular matching of interpolation expressions
        const regMusache = /\{\{\s*(\S+)\s*\}\}/

        // Proves that the current node is a text child node and requires regular replacement
        if (node.nodeType === 3) {
            // Note: Text subnode is also a dom object
            const text = node.textContent

            // Perform regular string matching and extraction
            const execResult = regMusache.exec(text)
            // console.log(execResult);
            if (execResult) {
                const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)

                node.textContent = text.replace(regMusache, value)
                // Create a Watcher instance at this time
                new Watcher(vm, execResult[1], (newValue) => {
                    // Update dom
                    node.textContent = text.replace(regMusache, newValue)
                })
            }

            // Terminate recursion
            return
        }

        // Prove that the current node is an input box
        if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
            const attrs = Array.from(node.attributes)
            const findResult = attrs.find(x => x.name === 'v-model')

            // Explanation has the attribute v-model
            if (findResult) {
                // Get the current v-model value
                const expStr = findResult.value

                const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
                // However, if you assign the value this way directly, the value in the Re-modify Value text box will not change. You need to add a Watcher instance to turn on publish-subscribe mode
                node.value = value

                // Add watcher instance
                new Watcher(vm, expStr, (value) => {
                    node.value = value
                })


                // Listen for input input events to get the latest values and update the latest values to vm
                node.addEventListener('input',(e) => {
                    const keyArr = expStr.split('.')
                    const obj = keyArr.slice(0,keyArr.length-1).reduce((newObj,k) => newObj[k],vm)
                    obj[keyArr[keyArr.length-1]] = e.target.value
                })

            }
        }

        // The proof is not a text node and needs to be processed recursively
        node.childNodes.forEach(child => {
            replace(child)
        })
    }
}


// Class for collecting subscribers
class Dep {
    constructor() {
        this.subs = []
    }

    addSub(watcher) {
        this.subs.push(watcher)
    }

    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}

// Subscriber's class
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb

        // The following three lines of magic code are responsible for saving the Watcher instance created to the subs of the Dep instance
        Dep.target = this
        key.split('.').reduce((newObj, k) => newObj[k], vm)
        Dep.target = null
    }

    update() {
        // Get the latest value and pass it to the callback function
        const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)

        this.cb(value)
    }
}


Operation effect:

Want a smart brain!

Topics: Javascript Vue Vue.js