Vue Source Code Learning (1) Responsive Principle
Vue is an MVVM framework, and the most appealing thing about it is its responsiveness.
Official explanation
First, let’s take a look at the explanation of the reactive principle in the official doc.
How to track change
When you pass a plain JavaScript object to a Vue instance as the’data 'option, Vue will iterate over all the properties of the object and use Object.defineProperty
Convert all these properties to getter/setterObject.defineProperty is a non-shim feature in ES5, which is why Vue does not support IE8 and earlier browsers.
These getters/setters are invisible to the user, but internally they allow Vue to track dependencies and notify changes when properties are accessed and modified. It should be noted here that different browsers format getters/setters differently when printing data objects in the Console, so it is recommended to install vue-devtools To get a more user interface friendly to inspection data.
Each component instance corresponds to a watcher instance, which records the data properties “touched” as dependencies during the component rendering process. Later, when the setter of the dependency is triggered, the watcher will be notified so that its associated component can be re-rendered.
The process of the above figure is roughly like this. When a component is instantiated, it will first define getters and setters for all the data in the data, and then trigger the render function. In the process of rendering into virtual dom, it will trigger the rendering process. The getters of those data will be added to the Watcher in the getter. This step is called dependency collection. When a certain dependent data changes, the Watcher will be notified to re-trigger the render function to re-render the page.
Considerations for detecting changes
Due to JavaScript limitations, Vue ** cannot detect changes in ** arrays and objects. However, we still have some ways to circumvent these limitations and ensure their responsiveness.
Vue cannot detect the addition or removal of properties. Since Vue performs getter/setter conversions on properties when the instance is initialized, the property must exist on the’data 'object for Vue to convert it to reactive. For example:
1 | var vm = new Vue({ |
For already created instances, Vue does not allow dynamic addition of root-level reactive properties. However, you can use the Vue.set (object, propertyName, value) method to add reactive properties to nested objects. For example, for:
1 | Vue.set(vm.someObject, 'b', 2) |
You can also use the vm. $set instance method, which is also an alias for the global Vue.set method:
1 | this.$set(this.someObject,'b',2) |
Sometimes you may need to assign multiple new properties to an existing object, such as using Object.assign () or _ (). However, the new property added to the object in this way will not trigger an update. In this case, you should create a new object with the original object and the property of the object you want to blend in.
1 | // 代替 `Object.assign(this.someObject, { a: 1, b: 2 })` |
For an array
Vue cannot detect changes in the following arrays:
- When you use an index to set an array item directly, for example: ‘vm.items [indexOfItem] = newValue’
- When you modify the length of the array, for example: ‘vm.items.length = newLength’
For example:
1 | var vm = new Vue({ |
In order to solve the first type of problem, the following two methods can achieve the same effect as’vm.items [indexOfItem] = newValue ', and also trigger status updates in a responsive system:
1 | // Vue.set |
You can also use vm.$set
Example method, which is an alias for the global method Vue.set:
1 | vm.$set(vm.items, indexOfItem, newValue) |
To solve the second type of problem, you can use’splice ':
1 | vm.items.splice(newLength) |
Source code parsing
1 | new Vue({ |
When we write this line of code, vue performs dependency tracking on the obj object we defined in data.
By implementing the new Observer (obj)
1 | //After the above code, our obj object will look like the following |
Observe
defineReactive
1 | function defineReactive(obj, key, val) { |
Observe function
1 | /** |
Observer类
1 | /** |
This involves Dep, and we also implement Dep.
Dep
1 | class Dep { |
The Observer class mainly does the following
- Traverse each property under data. If it is an object, execute new Observer () and add the ob property on the object, the value of which is an instance of Observer
- Hijack the change of object properties. When getting the getter, get the dep instance of the Observer instance and execute dep.depend (). The code is as follows
1 | const ob = this.__ob__ |
Take a look at what dep.depend () does
1 | this.subs.push(Dep.target) |
Add Dep.target to the subscription array, this.subs
That is, as long as we
1 | Dep.target =function test(){} |
Watcher
The implementation of this watcher is a simplified version, and even some of the logic defined in other functions is directly placed here, just for better understanding.
For a detailed explanation of Watcher, see my other blogVue的数据驱动原理
1 | const Dep = require('./Dep') |
Understanding’Watcher 'from a small example
1 | const obj = { |
The process is as follows:
- Observe the obj object first ('new Observer (obj) ')
- When instantiating’Watcher ‘, it will execute’ Dep.target = this’, and then execute’this.vm [exp] ', that is, take the value once, then it will trigger the getter and add itself (Watcher instance) to the subscriber array of dep
1 | get: function reactiveGetter() { |
Finally, when changing the data, trigger’setter’
1 | set: function reactiveSetter(newVal) { |
Execute’ob.dep.notify () ’
1 | notify() { |
Traverse, the subscriber (subs) executes the callback function, and the whole process ends
Dependency collection process
- add getters and setters to each property in the data via the observer function
- Add a new Watcher, Watcher construction process, will first assign the Dep.target to itself, and then manually call the getter again, so that you can trigger the getter to add Dep.target to the subs logic
- Once a property is modified, it will trigger notify in the setter to notify all added subs (that is, all Watcher instances) to call the update function.
Dependency logout process
I don’t know if you have thought about it, the above process is about how vue collects dependencies, how to trigger the update of the rendering watcher when the dependency changes, but we also know that due to the existence of v-if and the like, when the condition is true, we may collect some dependencies, but when the condition is false, these dependency changes should not trigger re-rendering, then these dependencies should be logged off.
So how does Vue do it?
We already know that when the data changes, 'dep.notify () ’ will be triggered, which will trigger’update 'of all watchers subscribed to the dep.
1 | update () { |
The’get 'method will be called again here
1 | get () { |
The last step, ‘cleanupDeps’, is actually used to remove invalid dependencies. It is actually used in conjunction with’addDep ’
1 | addDep (dep: Dep) { |
Each time a dependency is collected, the id of the dep will be saved to the newDepIds array. After the update is triggered, all the dependencies (this.deps) from the last update will be searched. If it is no longer in newDepIds, the dep will be removed
Vue3 Responsive Upgrade
Vue3 reuses ES6’s Proxy syntax to update the response, which can better handle the array update mentioned above.
https://sunra.top/posts/e5782665/
Reference article: