Vue2.x-response principle
1. Application of defineproperty
In vue2 X response uses defineProperty for data hijacking, so we must have a certain understanding of it. Let's first understand its usage. Here, we use defineProperty to simulate data in Vue.
<body> <div id="app"></div> <script> // Simulate Vue data let data = { msg: '', } // Simulate Vue instances let vm = {} // Hijack the msg of vm Object.defineProperty(vm, 'msg', { // get data get() { return data.msg }, // Set msg set(newValue) { // If the values passed in are equal, they do not need to be modified if (newValue === data.msg) return // Modify data data.msg = newValue document.querySelector('#app').textContent = data.msg }, }) // This calls defineproperty VM MSG set vm.msg = '1234' </script> </body>
You can see VM MSG data is responsive
2.defineProperty modifies multiple parameters to be responsive
Modify multiple parameters
After looking at the above methods, only one attribute can be modified. In fact, there can't be only one data in our data. Why don't we define a method to traverse the data in data into a response.
<body> <div id="app"></div> <script> // Simulate Vue data let data = { msg: 'ha-ha', age: '18', } // Simulate Vue instances let vm = {} // Convert multiple attributes into a response function proxyData() { // Take out [msg,age] for each item in data Object.keys(data).forEach((key) => { // Data hijacking of vm properties Object.defineProperty(vm, key, { // enumerable enumerable: true, // Configurable configurable: true, // get data get() { return data[key] }, // Set attribute value set(newValue) { // If the values passed in are equal, they do not need to be modified if (newValue === data[key]) return // Modify data data[key] = newValue document.querySelector('#app').textContent = data[key] }, }) }) } // Call method proxyData(data) </script> </body>
3.Proxy
Use Proxy in Vue3 to set the properties of a response
Let's first understand the two parameters of Proxy
new Proxy(target,handler)
- Target: the target object to be wrapped by Proxy (it can be any type of object, including a native array, a function, or even another Proxy)
- Handler: an object that usually takes a function as an attribute. The functions in each attribute define the behavior of the handler when performing various operations
Actually, vue2 The logic of X implementation is similar, but the implementation method is different
Then put the code
<body> <div id="app"></div> <script> // Simulate Vue data let data = { msg: '', age: '', } // Simulate an instance of Vue // Proxy first let vm = new Proxy(data, { // get() get value // target represents the object that needs proxy, which means data // Key is the key of the object get(target, key) { return target[key] }, // Set value // newValue is the set value set(target, key, newValue) { // Also, first judge whether it saves performance as the previous value if (target[key] === newValue) return // Set the value target[key] = newValue document.querySelector('#app').textContent = target[key] }, }) </script> </body>
Methods to trigger set and get
// set method triggered vm.msg = 'haha' // The get method was triggered console.log(vm.msg)
4. Publish subscribe mode
The Vue response is applied to the publish subscribe mode. Let's learn about it first
First of all, there are three roles
Publisher, subscriber and signal center take a real example. The author (publisher) writes an article and sends it to Nuggets (signal center). Nuggets can process the article and push it to the home page, and then their bosses (subscribers) can subscribe to the article
An example in Vue is EventBus $on $emit
Let's simply imitate Vue's event bus
<body> <div id="app"></div> <script> class Vue { constructor() { // Used to store events // Stored example this subs = { 'myclick': [fn1, fn2, fn3] ,'inputchange': [fn1, fn2] } this.subs = {} } // Implement the $on method. Type is the type of task queue and fn is the method $on(type, fn) { // Judge whether there is a method queue of the current type in the sub if (!this.subs[type]) { // If not, add an empty array by default this.subs[type] = [] } // Add method to this type this.subs[type].push(fn) } // Implement the $emit method $emit(type) { // First, we have to judge whether the method exists if (this.subs[type]) { // Get parameters const args = Array.prototype.slice.call(arguments, 1) // Loop queue call fn this.subs[type].forEach((fn) => fn(...args)) } } } // use const eventHub = new Vue() // Use $on to add a sum type method to sub ['sum '] eventHub.$on('sum', function () { let count = [...arguments].reduce((x, y) => x + y) console.log(count) }) // Trigger sum method eventHub.$emit('sum', 1, 2, 4, 5, 6, 7, 8, 9, 10) </script> </body>
5. Observer mode
Differences from publish subscriptions
Different from publishing subscribers, publishers and subscribers (observers) in observers are interdependent. Observers must be required to subscribe to content change events, while publishing subscribers are scheduled by the scheduling center. Let's take a look at how the observer mode is interdependent. Here's a simple example.
<body> <div id="app"></div> <script> // target class Subject { constructor() { this.observerLists = [] } // Add observer addObs(obs) { // Method for judging whether the observer has and exists updating subscription if (obs && obs.update) { // Add to observer list this.observerLists.push(obs) } } // Notify the observer notify() { this.observerLists.forEach((obs) => { // Each observer updates the event after receiving the notification obs.update() }) } // Empty observer empty() { this.subs = [] } } class Observer { // Define observer content update events update() { // The logic to handle in the update event console.log('Target updated') } } // use // Create target let sub = new Subject() // Create observer let obs1 = new Observer() let obs2 = new Observer() // Add observers to the list sub.addObs(obs1) sub.addObs(obs2) // When the target is turned on, each observer will trigger an update event sub.notify() </script> </body>
6. Simulate the response principle of Vue
Here is a small and simple Vue, which mainly realizes the following functions
- Receive initialization parameters. Here are just a few simple examples el data options
- By private method_ proxyData registers data with Vue and transfers it to getter setter
- Use observer to turn the attributes in data into response and add them to itself
- Use the observer method to listen for all attribute changes of data to update the view through observer mode
- Use the compiler to compile the difference expression between the instruction and the text node on the element node
1.vue.js
Get el data here
Pass_ proxyData registers the data attribute with Vue and converts it into a getter setter
/* vue.js */ class Vue { constructor(options) { // The object passed in is not empty by default this.$options = options || {} // Get el this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // Get data this.$data = options.data || {} // Call_ proxyData handles properties in data this._proxyData(this.$data) } // Register the properties in data with Vue _proxyData(data) { Object.keys(data).forEach((key) => { // Data hijacking // Add the attribute of each data to Vue and convert it into getter setter method Object.defineProperty(this, key, { // Settings can be enumerated enumerable: true, // Settings can be configured configurable: true, // get data get() { return data[key] }, // Set data set(newValue) { // Judge whether the new value is equal to the old value if (newValue === data[key]) return // Set new value data[key] = newValue }, }) }) } }
2.observer.js
Here, the attribute in data is changed into a response and added to itself. Another main function is the observer mode in Section 4 Dep.js will be used in detail
/* observer.js */ class Observer { constructor(data) { // Used to traverse data this.walk(data) } // Convert traversal data to response walk(data) { // Judge whether data is empty and object if (!data || typeof data !== 'object') return // Traversal data Object.keys(data).forEach((key) => { // Turn to responsive this.defineReactive(data, key, data[key]) }) } // Turn to responsive // Pay attention to and Vue JS is written differently // vue. In JS, the attribute is given to Vue and converted to getter setter // Here is to convert the property in data to getter setter defineReactive(obj, key, value) { // If it is an object type, the call to walk becomes a response. If it is not an object type, it will be return ed directly in the walk this.walk(value) // Save this const self = this Object.defineProperty(obj, key, { // Set enumerable enumerable: true, // Settings configurable configurable: true, // Get value get() { return value }, // Set value set(newValue) { // Judge whether the old value and the new value are equal if (newValue === value) return // Set new value value = newValue // If newValue is an object, the properties in the object should also be set to responsive self.walk(newValue) }, }) } }
Pay attention to the order of words introduced in html
<script src="./js/observer.js"></script> <script src="./js/vue.js"></script>
Then in Vue Using Observer in JS
/* vue.js */ class Vue { constructor(options) { ... // Use the observer to convert the data in the data into a response new Observer(this.$data) } // Register the properties in data with Vue _proxyData(data) { ... } }
See here, why did you do two repetitive operations? Repetitively convert the attribute of data into response twice
In obsever JS adds all the attributes of data to the data itself, changes the response expression into the getter setter method
In Vue JS also adds all attributes of data to Vue so that future aspect operations can be accessed directly by Vue instances or by this in Vue
Use examples
<body> <div id="app"></div> <script src="./js/observer.js"></script> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: '123', age: 21, }, }) </script> </body>
In this way, all data attributes exist in Vue and $data and are responsive
3.compiler.js
comilper.js implements the compilation of text node and element node instructions in this file, mainly for example. Of course, this written very simple instruction mainly implements v-text and V-model
/* compiler.js */ class Compiler { // vm refers to Vue instances constructor(vm) { // Get vm this.vm = vm // Get el this.el = vm.$el // Compile template this.compile(this.el) } // Compile template compile(el) { // Get the child node. If you use forEach traversal, turn the pseudo array into a true array let childNodes = [...el.childNodes] childNodes.forEach((node) => { // Compile according to different node types // Text type node if (this.isTextNode(node)) { // Compile text node this.compileText(node) } else if (this.isElementNode(node)) { //Element node this.compileElement(node) } // Determine whether there are child nodes and consider recursion if (node.childNodes && node.childNodes.length) { // Continue compiling templates recursively this.compile(node) } }) } // Compile text node (simple implementation) compileText(node) { // The core idea is to find the variables in the regular expression by removing {}} // Then go to Vue to find this variable and assign it to node textContent let reg = /\{\{(.+?)\}\}/ // Gets the text content of the node let val = node.textContent // Judge whether there is {} if (reg.test(val)) { // Get the content of group 1, that is, remove the front and back spaces let key = RegExp.$1.trim() // Replace and assign to node node.textContent = val.replace(reg, this.vm[key]) } } // Compile element nodes. Only instructions are processed here compileElement(node) { // Get all the attributes on the element node for traversal ![...node.attributes].forEach((attr) => { // Get property name let attrName = attr.name // Determine whether it is an instruction starting with v - if (this.isDirective(attrName)) { // Remove v- for easy operation attrName = attrName.substr(2) // The value of the get instruction is msg in v-text = "msg" // msg as a key, go to Vue to find this variable let key = attr.value // Instruction operation execution instruction method // There are many vue instructions. In order to avoid a large number of if judgments, a uapdate method is written here this.update(node, key, attrName) } }) } // Add instruction method and execute update(node, key, attrName) { // For example, adding textUpdater is used to handle the v-text method // We should call the built-in textUpdater method // It doesn't matter what you add a suffix, but you need to define the corresponding method let updateFn = this[attrName + 'Updater'] // If this built-in method exists, it can be called updateFn && updateFn(node, key, this.vm[key]) } // Write the corresponding specified method in advance, such as this v-text // It is the same as Vue's textUpdater(node, key, value) { node.textContent = value } // v-model modelUpdater(node, key, value) { node.value = value } // Determine whether the attribute of an element is a vue instruction isDirective(attr) { return attr.startsWith('v-') } // Determine whether it is an element node isElementNode(node) { return node.nodeType === 1 } // Determine whether it is a text node isTextNode(node) { return node.nodeType === 3 } }
4.dep.js
Write a Dep class, which is equivalent to the publisher in the observer. Each responsive attribute will create such a Dep object, which is responsible for collecting the Watcher object of the dependent attribute (an operation done when using responsive data).
When we update the responsive property in the setter, we will call the notify method in Dep to send an update notification.
Then call the update in Watcher to update the view (notify the observer to call the observer's update to update the view when the data changes).
Generally speaking, DEP (here refers to publisher) is responsible for collecting dependencies, adding observers (here refers to Watcher), and then notifying observers when setter data is updated.
Let's tell you which of the following stages is the implementation stage
Write the Dep class first
/* dep.js */ class Dep { constructor() { // Storage observer this.subs = [] } // Add observer addSub(sub) { // Determine whether the observer exists and owns the update method if (sub && sub.update) { this.subs.push(sub) } } // Notification method notify() { // Trigger update method for each observer this.subs.forEach((sub) => { sub.update() }) } }
At observer Using Dep in JS
Add Dep.target (observer) in get
Trigger notify in set
/* observer.js */ class Observer { ... } // Convert traversal data to response walk(data) { ... } // Here is to convert the property in data to getter setter defineReactive(obj, key, value) { ... // Create Dep object let dep = new Dep() Object.defineProperty(obj, key, { ... // Get value get() { // Add an observer object here. Dep.target represents the observer Dep.target && dep.addSub(Dep.target) return value }, // Set value set(newValue) { if (newValue === value) return value = newValue self.walk(newValue) // Trigger notification update view dep.notify() }, }) } }
5.watcher.js
The function of watcher is to update the data after update is updated.
/* watcher.js */ class Watcher { constructor(vm, key, cb) { // vm is a Vue instance this.vm = vm // key is an attribute in data this.key = key // How to update the view with cb callback function this.cb = cb // Store the observer's in Dep.target Dep.target = this // Compare the old data when updating the view // Another point is that vm[key] triggers the get method at this time // Previously, the observer was added to dep.subs through dep.addSub(Dep.target) in get this.oldValue = vm[key] // Dep.target doesn't need to exist because the above operations have been saved Dep.target = null } // The necessary methods in the observer are used to update the view update() { // Get new value let newValue = this.vm[this.key] // Compare old and new values if (newValue === this.oldValue) return // Call the specific update method this.cb(newValue) } }
So where to create Watcher? Remember in compiler Do you compile text nodes in JS
After compiling the text node, add a Watcher here
In addition, the v-text v-model instruction adds a Watcher when compiling element nodes
/* compiler.js */ class Compiler { // vm refers to Vue instances constructor(vm) { // Get vm this.vm = vm // Get el this.el = vm.$el // Compile template this.compile(this.el) } // Compile template compile(el) { let childNodes = [...el.childNodes] childNodes.forEach((node) => { if (this.isTextNode(node)) { // Compile text node this.compileText(node) } ... } // Compile text node (simple implementation) compileText(node) { let reg = /\{\{(.+)\}\}/ let val = node.textContent if (reg.test(val)) { let key = RegExp.$1.trim() node.textContent = val.replace(reg, this.vm[key]) // Create observer new Watcher(this.vm, key, newValue => { node.textContent = newValue }) } } ... // v-text textUpdater(node, key, value) { node.textContent = value // Create observer 2 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater(node, key, value) { node.value = value // Create observer new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // Here, the bidirectional binding is implemented to listen to input events and modify the properties in data node.addEventListener('input', () => { this.vm[key] = node.value }) } }
When we change the responsive attribute, we trigger the set() method, and then the publisher dep.notify method starts. We get all the observer watcher instances to execute the update method, call the callback function cb(newValue) method, and pass the new value to cb(). When cb method is a specific view updating method to update the view
For example, the third parameter cb method in the above example.
new Watcher(this.vm, key, newValue => { node.textContent = newValue })
Another point is to realize the two-way binding of v-model
Not only should the view be updated by modifying the data, but also the input event should be added to the node to change the properties in the data
To achieve the effect of two-way binding
7. Test what you write
So far, both responsive and bidirectional binding have been basically implemented. Then write an example to test
<body> <div id="app"> {{msg}} <br /> {{age}} <br /> <div v-text="msg"></div> <input v-model="msg" type="text" /> </div> <script src="./js/dep.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compiler.js"></script> <script src="./js/observer.js"></script> <script src="./js/vue.js"></script> <script> let vm = new Vue({ el: '#app', data: { msg: '123', age: 21, }, }) </script> </body>
OK basically realizes the responsive principle through observer mode.
8. Five document codes
Here directly paste 5 files and codes. Some places above have been omitted. The following is a complete one for everyone to read
vue.js
/* vue.js */ class Vue { constructor(options) { // The object passed in is not empty by default this.$options = options || {} // Get el this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // Get data this.$data = options.data || {} // Call_ proxyData handles properties in data this._proxyData(this.$data) // Use the observer to convert the data in the data into a response new Observer(this.$data) // Compile template new Compiler(this) } // Register the properties in data with Vue _proxyData(data) { Object.keys(data).forEach((key) => { // Data hijacking // Add the attribute of each data to Vue and convert it into getter setter method Object.defineProperty(this, key, { // Settings can be enumerated enumerable: true, // Settings can be configured configurable: true, // get data get() { return data[key] }, // Set data set(newValue) { // Judge whether the new value is equal to the old value if (newValue === data[key]) return // Set new value data[key] = newValue }, }) }) } }
observer.js
/* observer.js */ class Observer { constructor(data) { // Used to traverse data this.walk(data) } // Convert traversal data to response walk(data) { // Judge whether data is empty and object if (!data || typeof data !== 'object') return // Traversal data Object.keys(data).forEach((key) => { // Turn to responsive this.defineReactive(data, key, data[key]) }) } // Turn to responsive // Pay attention to and Vue JS is written differently // vue. In JS, the attribute is given to Vue and converted to getter setter // Here is to convert the property in data to getter setter defineReactive(obj, key, value) { // If it is an object type, the call to walk becomes a response. If it is not an object type, it will be return ed directly in the walk this.walk(value) // Save this const self = this // Create Dep object let dep = new Dep() Object.defineProperty(obj, key, { // Set enumerable enumerable: true, // Settings configurable configurable: true, // Get value get() { // Add an observer object here. Dep.target represents the observer Dep.target && dep.addSub(Dep.target) return value }, // Set value set(newValue) { // Judge whether the old value and the new value are equal if (newValue === value) return // Set new value value = newValue // If newValue is an object, the properties in the object should also be set to responsive self.walk(newValue) // Trigger notification update view dep.notify() }, }) } }
compiler.js
/* compiler.js */ class Compiler { // vm refers to Vue instances constructor(vm) { // Get vm this.vm = vm // Get el this.el = vm.$el // Compile template this.compile(this.el) } // Compile template compile(el) { // Get the child node. If you use forEach traversal, turn the pseudo array into a true array let childNodes = [...el.childNodes] childNodes.forEach((node) => { // Compile according to different node types // Text type node if (this.isTextNode(node)) { // Compile text node this.compileText(node) } else if (this.isElementNode(node)) { //Element node this.compileElement(node) } // Determine whether there are child nodes and consider recursion if (node.childNodes && node.childNodes.length) { // Continue compiling templates recursively this.compile(node) } }) } // Compile text node (simple implementation) compileText(node) { // The core idea is to find the variables in the regular expression by removing {}} // Then go to Vue to find this variable and assign it to node textContent let reg = /\{\{(.+?)\}\}/ // Gets the text content of the node let val = node.textContent // Judge whether there is {} if (reg.test(val)) { // Get the content of group 1, that is, remove the front and back spaces let key = RegExp.$1.trim() // Replace and assign to node node.textContent = val.replace(reg, this.vm[key]) // Create observer new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } // Compile element nodes. Only instructions are processed here compileElement(node) { // Get all the attributes on the element node for traversal ![...node.attributes].forEach((attr) => { // Get property name let attrName = attr.name // Determine whether it is an instruction starting with v - if (this.isDirective(attrName)) { // Remove v- for easy operation attrName = attrName.substr(2) // The value of the get instruction is msg in v-text = "msg" // msg as a key, go to Vue to find this variable let key = attr.value // Instruction operation execution instruction method // There are many vue instructions. In order to avoid a large number of if judgments, a uapdate method is written here this.update(node, key, attrName) } }) } // Add instruction method and execute update(node, key, attrName) { // For example, adding textUpdater is used to handle the v-text method // We should call the built-in textUpdater method // It doesn't matter what you add a suffix, but you need to define the corresponding method let updateFn = this[attrName + 'Updater'] // If this built-in method exists, it can be called updateFn && updateFn.call(this, node, key, this.vm[key]) } // Write the corresponding specified method in advance, such as this v-text // It is the same as Vue's textUpdater(node, key, value) { node.textContent = value // Create observer new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } // v-model modelUpdater(node, key, value) { node.value = value // Create observer new Watcher(this.vm, key, (newValue) => { node.value = newValue }) // Two way binding is implemented here node.addEventListener('input', () => { this.vm[key] = node.value }) } // Determine whether the attribute of an element is a vue instruction isDirective(attr) { return attr.startsWith('v-') } // Determine whether it is an element node isElementNode(node) { return node.nodeType === 1 } // Determine whether it is a text node isTextNode(node) { return node.nodeType === 3 } }
dep.js
/* dep.js */ class Dep { constructor() { // Storage observer this.subs = [] } // Add observer addSub(sub) { // Determine whether the observer exists and owns the update method if (sub && sub.update) { this.subs.push(sub) } } // Notification method notify() { // Trigger update method for each observer this.subs.forEach((sub) => { sub.update() }) } }
watcher.js
/* watcher.js */ class Watcher { constructor(vm, key, cb) { // vm is a Vue instance this.vm = vm // key is an attribute in data this.key = key // How to update the view with cb callback function this.cb = cb // Store the observer's in Dep.target Dep.target = this // Compare the old data when updating the view // Another point is that vm[key] triggers the get method at this time // Previously, the observer was added to dep.subs through dep.addSub(Dep.target) in get this.oldValue = vm[key] // Dep.target doesn't need to exist because the above operations have been saved Dep.target = null } // The necessary methods in the observer are used to update the view update() { // Get new value let newValue = this.vm[this.key] // Compare old and new values if (newValue === this.oldValue) return // Call the specific update method this.cb(newValue) } }
QQ:505417246
Wechat: 18331092918
WeChat official account: Code program life
Personal blog: http://rayblog.ltd