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!