It doesn’t matter what you say, hope, expect, or think, only what you do counts —— Born. Tracy
preface
This is a Vue source series of articles, recommended from the first article Vue source series (a) : Vue source interpretation of the correct posture to start reading. Article is my personal learning source code of a process, here to share the hope to help you.
This article will explain Vue asynchronous update mechanism and nextTick principle.
As you all know, asynchronous updates are at the heart of Vue, along with the reactive principle. In our use of Vue, the majority of Watcher updates were handled asynchronously. NextTick is at the heart of asynchronous updates.
Next I will start with the source code, and then explain with examples, so that partners can understand a core principle of this Vue.
The source code parsing
Source code parsing we need to find an entrance, that entrance is what?
In the previous article Vue source code series (three) : data responsive principle 🔥🔥 we said Vue data responsive principle, which in dep.js (that is, “code block 8”), we said a method: notify(), I was on his resolution is: notification update. It is the entry point to this article, so if you don’t remember, please refer back to the previous article.
Next, the notify() method is used to interpret the source code of this chapter.
dep.js
The code block1
// src/core/observer/dep.js.// Iterate through all the watchers in the DEP and perform their update
notify () {
// Get all the watcher collected by deP
const subs = this.subs.slice()
// Do sorting in non-production environments and execute synchronously
if(process.env.NODE_ENV ! = ='production' && !config.async) {
subs.sort((a, b) = > a.id - b.id)
}
// Iterate over all watcher and execute their respective update() methods
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
...
Copy the code
watcher.js
Let’s see what watcher does with the update() method it calls.
The code block2
// src/core/observer/watcher.js./ / update
update () {
if (this.lazy) {
// Lazy execution goes here, for example: computed ()
this.dirty = true
} else if (this.sync) {
Update the view executes the following run function
this.run()
} else {
// Put watcher on the observer queue
queueWatcher(this)}}// Update the view
run () {
if (this.active) {
// Call the get method
const value = this.get()
if( value ! = =this.value ||
isObject(value) ||
this.deep
) {
// Update the old value to the new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "The ${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
/ / render watcher
this.cb.call(this.vm, value, oldValue)
}
}
}
}
...
Copy the code
scheduler.js
Continue to look at the queueWatcher method that puts watcher on the observer queue
The code block3】
// /src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
/** * When we call a callback from watcher, we set the has flag to null ** if has[id] does not exist, If has[id] exists and is null, it is true * If has[id] exists and is true, it is false * Return */ if the watcher has not been flushed
If watcher already exists, it will not be queued twice
if (has[id] == null) {
// Set watcher to true again to indicate that there is already one watcher in the queue
has[id] = true
if(! flushing) {// If not in the refresh queue, the watcher is pushed into the queue
queue.push(watcher)
} else {
// If in the refresh queue, the current watcher.id is traversed
// This loop is actually dealing with boundary cases. That is, during watcher queue update, the user updates a watcher in the queue again
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
// insert yourself into the next position if you find something smaller than yourself
queue.splice(i + 1.0, watcher)
}
if(! waiting) {// Ensure that nextTick executes only once through waiting
waiting = true
// Flush the scheduling queue when executing synchronously in a non-production environment
if(process.env.NODE_ENV ! = ='production' && !config.async) {
flushSchedulerQueue()
return
}
FlushSchedulerQueue will eventually be put into nextTick and pushed into the asynchronous queue
nextTick(flushSchedulerQueue)
}
}
}
Copy the code
next-tick.js
Finally, let’s look at what nextTick does.
The code block4】
// src/core/util/next-tick.js
// Introduce some required methods
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
// Whether to use microtasks
export let isUsingMicroTask = false
// A queue of events that need to be processed
const callbacks = []
// If timerFunc has already been pushed to the task queue, it will not be pushed
let pending = false
// Finally executes the callback passed in by the nextTick method (executes events in the event queue)
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
// Iterate through the callbacks, executing the flushSchedulerQueue function stored in it
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
/** ** * /** * * * Here we have async deferring wrappers using microtasks. * In 2.5 we use (macro) tasks (In) Combination with microtasks). * In version 2.5 we used a combination of macro tasks and microtasks * However, It has subtle problems when state is changed right before repaint * Also, using (macro) tasks in event handlers would cause some weird behaviors * The use of macro tasks in event handlers leads to some unavoidable strange behavior. * A major drawback of this tradeoff is that there are some scenarios where microtasks come into play have too high a priority and fire in between supposedly sequential events or even between bubbling of the same event * A major disadvantage of this trade-off is that, in some cases, the priority of microtasks is too high, triggering * */ between supposed sequential events, or even between bubbling of the same event
// Set a function pointer, add the pointer to the queue, wait until the main thread task is finished, add the timerFunc function of the queue to the execution stack
let timerFunc
Promise => MutationObserver => setImmediate => setTimeout */
// First determine whether to support promises natively
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// Next, determine whether MutationObserver is supported natively
// MutationObserver: This method provides the ability to monitor changes made to the DOM tree
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// Use MutationObserver to flushCallbacks if MutationObserver is supported
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () = > {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
// Again determine whether setImmediate is native supported
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
timerFunc = () = > {
SetImmediate Executes flushCallbacks with setImmediate if native supports setImmediate
// This method is used to place long-running operations in a callback function that is executed as soon as the browser completes other statements.
setImmediate(flushCallbacks)
}
// Use setTimeout when none is supported
} else {
timerFunc = () = > {
setTimeout(flushCallbacks, 0)}}// Wrap the callback function cb as an arrow function push into the event queue callbacks
export function nextTick (cb? :Function, ctx? :Object) {
let _resolve
// Add arrow functions as arguments to the event queue and use callbacks to store wrapped CB functions
callbacks.push(() = > {
if (cb) {
// Try catch wraps callback functions for error catching
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')}}else if (_resolve) {
_resolve(ctx)
}
})
// Pending is set to false only for flushCallbacks
Pending is the time when the flushCallbacks are added to the queue before execution
if(! pending) { pending =true
timerFunc()
}
if(! cb &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
}
Copy the code
Source code analysis summary
This is vUE asynchronous update source code and nextTick source code analysis. Let’s summarize a little bit:
- Asynchronous update mechanism is one of the core principles of VUE, mainly using the browser asynchronous task queue. (There will be an Event Loop article later, and the link will be posted after the article is OK.)
- Called after data updatedep.jsIn thenotify()traversedepAll of thewatcherAnd then execute theirsupdate()
- thenwatcher.jsIn theupdate() 将 watcherPut it in the observer queue,
- The last executionscheduler.jsIn thequeueWatcher()Finally, all update actions (flushSchedulerQueue) are put into flushSchedulerQueuenextTick, and pushed into an asynchronous queue to execute. In a callback, rightqueueWatcher()In thewatcherSort, and then perform the corresponding DOM update
Why do Vue data updates need to be asynchronous?
Use the following code as an example to expand the description
<template>
<div>
<h1 ref="h1" @click="handleMessage">{{ message }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Asynchronous update'}},methods: {
handleMessage() {
this.message = 'nextTick'
const _data = this.$refs['h1'].innerHTML
console.log(_data)
}
},
}
</script>
<style lang="scss" scoped></style>
Copy the code
The code above is intended to update the h1 content to nextTick when the h1 tag is clicked. But in the actual process, we will find that such a simple logic can not be realized, since it is not the result of our imagination after printing. As shown in figure left
As you can see from the figure above, on the first click, we print out the same valueAsynchronous updateRather thannextTick. It will only change on the second clicknextTick. So if we wanted to update and do something else on the first click, we wouldn’t be able to do that.
So why does this happen?
This is because vue updates the DOM asynchronously. When vue observes a change in data, it will be cached in an event loop, and will not render until the next event, after the JS engine has cleared the cache in the event queue.
So when we print, we can’t print the result we want. So what if we want to print the results we want? It’s actually pretty easy. Add onenextTickIs OK. The code on the left
<template>
<div>
<h1 ref="h1" @click="handleMessage">{{ message }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Asynchronous update'}},methods: {
handleMessage() {
this.message = 'nextTick'
this.$nextTick(() = >{
const _data = this.$refs['h1'].innerHTML
console.log(_data)
})
}
},
}
</script>
<style lang="scss" scoped></style>
Copy the code
The effect is as follows ↓
So the question is, why should Vue update data asynchronously? It doesn’t look like much. Let’s look at another example 🌰
<template>
<div>
<h1 ref="h1" @click="handleMessage">{{ value }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
value: 0}},methods: {
handleMessage() {
for(var i=0; i<=10; i++ ) {
this.value = i
console.log(this.value)
}
}
},
}
</script>
<style lang="scss" scoped></style>
Copy the code
See the same effect when we click on h1 ↓
As shown in the figure, we normally think that this.value goes from 0 to 10, and the view goes from 0 to 10, so the view just goes from 0 to 10 without any transition. This is why Vue updates data asynchronously:
Vue renders the entire component with each update. If it is synchronous, once the data property is changed, the corresponding Watcher will be triggered, and then the update method under the corresponding Watcher will be called to update the view, which will cause the rendering to be too frequent. For asynchronous update, Vue will update the view asynchronously after the data is updated in this round, which can greatly optimize performance.
This is the end of this article, I hope to help you. At the same time see here also hope XDM to present a small free love, thank you 🙏 🙏 🙏
Vue2. X series of source code articles also to this article so far, the basic routine vUE source interview questions are about the same, will continue to Vue3. X series of articles, I hope we continue to support 🙏 🙏 🙏.