The classic design idea of Vue is data-driven: data changes automatically update views. To achieve this, Vue needs to observe changes in the data and automatically execute the corresponding view update operation function (commonly known as the Render function) when it senses changes in the data. Thus the characteristics of responsive data are realized.
The overall process
From the figure above we can get an idea of the general flow of the reactive principle. For the specific implementation of responsive data, read the source code can know that Vue is through the following core modules to achieve:
- Observer
- Dep
- Watcher
- Scheduler
The following are the specific functions of each module and the problems to be solved.
Observer (Message)
Core function: Convert normal data (objects) to responsive data (objects)
The observed data are mainly divided into two types:
- The object itself
- Object properties
Object.defineProperty()
An Observer is a core module in a Vue system. It is a basic Constructor for implementing responsive data. Internally, each property of a normal Object is recursively iterated over and converted into a property with getter and setter methods using the Object manipulation API provided by Javascript — Object.defineProperty(). Thereafter, dependency collection and change notification are completed, respectively, when the property is read or modified. Vue has the ability to automatically sense changes in data.
Done before Creating
Note that Observer responsiveness occurs after beforeCreate in the Vue lifecycle and before creation.
Vue.observable()
Vue provides static methods: vue.Observable () that manually convert a common object into a responsive object. Such as:
var obj = {
a: 1.b: 2.c: {
d: 3.e: 4
},
f: [{a: 1.b: 2
},
3.4.5.6]}// Convert a common object into a responsive object using the static.observable method provided by Vue
var reactiveObj = Vue.observable(obj)
console.log(reactiveObj)
Copy the code
data
Vue does not allow you to dynamically add root-level reactive properties, so you need to declare all root-level reactive properties, even if the value is null, through the data field in the configuration before component instantiation. The benefits are as follows:
- Easier to maintain: Data objects are like a component’s state structure (Schema), declaring all reactive properties up front and helping developers understand and modify component logic later.
- A class of boundary cases in dependency tracking systems are eliminated.
- Make Vue instances work better with the type checking system.
A special case
Dynamically add or remove properties
This is because Vue performs getter/setter conversions for all properties that exist in data in the configuration when the instance is initialized.
Vue cannot automatically check for dynamically added or deleted properties.
Therefore, Vue provides the following ways to manually complete reactive data.
- Var. Set (target, key, val) or this.$set(target, key, val)
- Delete: vue. delete(target, key) or this.$delete(target, key)
- Batch operation:
this.reactiveObj = Object.assign({}, this.reactiveObj, obj)
Here’s an example:
< the template > < div class = "demo - wrapper" > < p > obj. A - > {{obj. A}}, obj. B - > {{obj. B}} < / p > <! -- Non-responsive data manipulation --> <! -- <button @click="obj.b = 2">add obj.b</button> <button @click="delete obj.a">delete obj.a</button> --> <! - responsive data operations - > < button @ click = "$set (obj, 'b', 2)" > add obj. B < / button > & have spent <button @click="$delete(obj, 'a')">delete obj.a</button> </div> </template> <script> export default { data() { return { obj: { a: 1, }, }; }}; </script>Copy the code
About array
Due to js limitations, Vue cannot detect the following array changes:
- When an index is used to change an array item directly, for example:
vm.arr[idx] = newValue
- When modifying an array length, for example:
vm.arr.length = newLength
Here’s an example:
<script> export default { data() { return { arr: [1, 2, 3, 4], }; }, created() { window.lesson4 = this; }, mounted() { this.arr[0] = 8; This.arr. Length = 2; // Not reactive}}; </script>Copy the code
To make the above array operations responsive, use the following method:
<script> export default { data() { return { arr: [1, 2, 3, 4], }; }, created() { window.lesson4 = this; }, mounted() {this.$set(arr, 0, 8); Vue. Set (arr, 0, 8); This.arr. Splice (0, 1, 8) this.arr. Splice (2) this.arr. </script>Copy the code
In addition to the static method vue.set () and instance method this.$set(), we can change the value of the array item in response. You can also use the array method -splice ().
This is because Vue intercepts and overwrites API operations that change the contents of arrays themselves, such as splice(), sort(), push(), POP (), reverse(), shift(), unshift(), and so on. When developers use these apis, reactive data can be triggered to update the view.
<script> export default{ data() { return { arr: [1, 2, 3, 4]}}, mounted() { console.log(this.arr._proto_ === Array.prototype) // => false console.log(this.arr._proto_._proto_ === Array.prototype) //=> true } } </script>Copy the code
The illustration is as follows:
Dep (Notification)
Dep, meaning Dependency, means Dependency. Is a module (Class) for dependency management of reactive data.
Problems solved by Dep
- When a getter for a reactive property is fired (access property), how do you collect dependencies — who is using “I”?
- When reactive property setters are triggered (fixing properties), how can updates be sent — notifying who uses “I”?
The dependency management process is analyzed as follows:
- When an object is turned into a responsive object, an instantiation DEP (
dep = new Dep()
) for managing dependencies. - Set a globally unique Watcher currently executed with dep. target.
- Is called when the property is accessed (triggering prop getter)
dep.depend()
, will be the current globalWatcherAdd to dependencies - Is called when a property is modified (triggering a prop setter)
dep.notify()
, iterates through the currently collected dependencies (Watcher) and invokes eachWatchertheupdate()
API, and then the update operation (the update function) is executed inside Watcher.
This involves a new concept, Watcher, which you’ll learn more about in the next section.
Watcher (who)
Question: When a responsive object property is accessed, how does the corresponding getter know who accessed it? Or: how does Dep know who uses it?
Imagine a scenario where some reactive data is used when a function is executed. Js cannot directly know that it is the function that uses the reactive data, so it cannot find the function and execute it again to complete the update when the data is updated.
Object.defineProperty(obj, 'a', {
get() {
dep.depend()
// Dep.depend () -> dep.target.adddep (dep) -> dep.addsup (dep.target) -> dep.subs adds a watcher
},
set() {
dep.notify()
// dep.notify() -> traverse dep.subs -> call watcher.update() -> Each watcher assigns itself to a Scheduler -> The Scheduler places the Watcher queue into the microtask queue of the event loop for asynchronous execution.}})// Example: how is the render function collected during component rendering
function render() {
// The render function is called with the response data obj.a
console.log(obj.a)
}
/ / # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
// 🙋 : how does obj.a know when the render function calls itself?
/ / # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
// Solution: Watcher
// use watcher () to render //
var watcher = new Watcher(render)
// first render
watcher.run()
/ / (3) at this time
Dep.target = watcher // dep. target is managed by a stack
// â‘£ run() -> render()
render()
Render (); // render(); // render();
dep.depend() -> ... -> dep.addSub(Dep.target)
// When data is updated, the deP of the corresponding data informs the dependent watcher of the update
dep.notify() -> ... -> dep.subs ... watcher.update() -> queueWatcher
// ⑤ The watcher to be updated assigns itself to a scheduler to execute and places it on the microtask queue.
scheduler -> nextTick -> queueWatcher -> watcher.run
Copy the code
To solve the above problem, Vue introduced a clever way to introduce a new concept – Watcher. Instead of collecting and distributing update operations (functions) directly, the Dep collects and distributes Watcher, and then performs update operations (functions) through Watcher. Therefore, each update operation (function) should be wrapped by creating a Watcher that performs the update through the Watcher agent.
Watcher can be understood as proxy objects that perform update operations (e.g., render function) when data is updated.
The schematic diagram of the principle is as follows:
To sum up:
-
For each Vue component instance, there is at least one Watcher that records the render function of that component
-
Watcher first executes the render function, collecting dependencies in the process (the responsive data used in the render function records the Watcher).
-
When data is updated, the Dep notifies all the Watchers it collects, and each Watcher then performs the specific update operation (function). After the update function is executed, the interface is re-rendered. And dependencies are collected again. Refer to the call flow:
props setter -> dep.notify() -> depWatcher.update() -> nextTickWatcherRun -> render function -> updateComponent
Scheduler
Question: When multiple reactive data updates occur, is it necessary to perform the same watcher update multiple times?Copy the code
Considering the above question, consider this: if a function given to Watcher uses a, B, C, and D, all four attributes record dependencies. The following code triggers four updates:
this.obj.a = 'new a'
this.obj.b = 'new b'
this.obj.c = 'new c'
this.obj.d = 'new d'
Copy the code
This is clearly not appropriate. Obviously, watcher should not be implemented too often for efficiency.
Thus, the concept of Scheduler is introduced.
How the Scheduler works: When the Dep notifies the Watcher of an update, the update cannot be performed immediately and the Watcher is handed over to a Scheduler to manage. Scheduler maintains a queue for the watcher waiting to be executed, and the same watcher exists only once. The watcher in the queue is then put into the nextTick of the event loop to execute.
Summary:
- When a component renders, the Render function is executed asynchronously in the microtask queue of the event loop.
Asynchronous update queue
When Vue listens for data changes, it opens a queue. Instead of rerendering immediately, the component buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once, thus avoiding unnecessary calculations and DOM manipulation.
In the next event loop, “Tick”, Vue flushs the queue and performs the actual (de-duplicated) work (updating the render).
To do this, Vue provides a listening interface for asynchronous updates — vue.nexttick (callback) or this.$nextTick(callback). When the data changes and the asynchronous DOM update is complete, the callback is called. Developers can manipulate the updated DOM in a callback.
For example 1
export default {
data() {
return {
a: 1.b: 2.c: 3.d: 4}; },methods: {
changeAllData() {
this.$nextTick(function () {
var pre = document.querySelector("pre");
console.log(pre.textContent);
});
this.a = this.b = this.c = this.d = 10;
this.$nextTick(function () {
var pre = document.querySelector("pre");
console.log(pre.textContent); }); }},render(h) {
console.log('render function')
return h('div', [
h('pre'.`The ${this.a}.The ${this.b}.The ${this.c}.The ${this.d}`),
h('button', {
on: {
click: () = > {
this.changeAllData()
}
}
}, 'change all data')])}};Copy the code
See how $NextTick works by rendering a NextTick component. In order to see how the render function is called when the component is rendering, the render function is given directly when the component is defined. When the button is clicked, two functions that read the INTERFACE Dom are written before and after data modification using the $nextTick tool method. As a result, the first $nextTick callback retrieves old data and the second $nextTick callback retrieves new data.
Analyze:
After the button is clicked, the steps to add the asynchronous queue are:
- The first one
$nextTick
, adds its own callback function (fn1) to the current asynchronous queue. - After the data is modified and updates are dispatched, the Scheduler adds the function (Fn2) that contains the watcher queue execution logic to the current asynchronous queue.
- The second
$nextTick
, has added its own callback function (fn3) to the current asynchronous queue.
When an asynchronous queue is executed, fn1, fn2, and fn3 are executed in sequence. The interface updates the latest data only after fn2 is executed. Therefore, the interface data obtained by FN1 and FN3 are old data and new data.
For example 2
<template>
<span>{{a}}</span>
</template>
<script>
export default {
data() {
return {
a: 'hello'
}
},
mounted() {
this.a = 'world'
console.log(this.$el.textContent) // -> 'hello'
this.$nextTick(function() {
console.log(this.$el.textContent) // -> 'world'
})
}
}
</script>
Copy the code
The code above, when set this.a = ‘world’, accesses the DOM element content, but is not updated. Immediately listen for DOM updates with this.$nextTick() and get the updated DOM content when listening for callback calls.
In addition, this.$nextTick() internally tries to use native Promise.then, MutationObserve, setImmediate, and setTimeout instead if the execution environment does not support it. And eventually returns a Promise object, so you can use async/await syntax instead of callback.
<script> export default { data() { return { a: 'hello' } }, Async Mounted () {this.a = 'world' console.log(this.$el.textContent) // -> 'hello' await this.$nextTick() console.log(this.$el.textContent) // -> 'world' } } </script>Copy the code
The last
If you find it helpful, give it a like! Your support is my biggest encouragement!
Wechat pay attention to “ride the wind and waves big front end”, find more interesting good front-end knowledge and actual combat.
If you have any comments or suggestions about this article, please feel free to discuss them in the comments section.