Vue updates asynchronously, as we all know, so we typically use nextTick to ensure that some business logic is executed after the update. I always thought I understood, but when I saw this question, I found that I did not seem to understand:
Title: The following print order? Name = ‘b’ Answer: the first question is 2, 1. The second question is 1, 2.
<template>
<div id="app">
{{name}}
</div>
</template>
<script>
export default {
data() {
return {
name: 'a'
}
},
mounted() {
this.name = 'b'
Promise.resolve().then(() => {
console.log(1)
})
this.$nextTick(() => {
console.log(2)
})
}
};
</script>
Copy the code
Did you get that right?
Let’s go to the source code to find the answer.
Where to find the entry to the update logic, naturally we should think of the set method of defineProperty, and that’s it:
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if(newVal === value || (newVal ! == newVal && value ! == value)) {return;
}
// #7981: for accessor properties without setter
if(getter && ! setter)return;
if (setter) {
setter.call(obj, newVal);
} else{ val = newVal; } childOb = ! shallow && observe(newVal); dep.notify(); },Copy the code
Here we assign the new value to val and call dep.notify() to tell Watcher to update:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)}}Copy the code
Here we go to queueWatcher(this) :
const id = watcher.id
// If 'Watcher' does not exist, it will only join the team once
if (has[id] == null) {
has[id] = true
if(! flushing) { queue.push(watcher) }else {
// When will I walk here?
For example, when executing a watch, assigning to reactive data triggers an update to another watch
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
// queue the flush
if(! waiting) { waiting =true
if(process.env.NODE_ENV ! = ='production' && !config.async) {
flushSchedulerQueue()
return
}
// Execute flushSchedulerQueue asynchronously
nextTick(flushSchedulerQueue)
}
}
Copy the code
First, the system checks whether the current Wathcer has been queued. If the Wathcer has been queued, the system does not process the Wathcer. Then determine if the queue is being refreshed, and if not, enqueue Watcher. Ignoring the rest of the logic and not analyzing nextTick, we just need to know that the flushSchedulerQueue will always execute at some point in the future, and look what it does:
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
// The flag is currently being refreshed
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// User Watchers are created before the render watcher) because the user wathcer is generated when the Vue is initialized, and the render wathcer is generated when the $mount
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) = > a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
// Take out one watcher at a time
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// The real operation is done by the run method
watcher.run()
// in dev build, check and stop circular updates.
if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break}}}}Copy the code
The user Watcher is generated when the Vue is initialized, and the render Wathcer is generated at $mount, so the user Watcher is executed before the component’s render Watcher. Then we iterate through watcher.run() :
run () {
if (this.active) {
// Execute get if the current watcher is render watcher
// This get will be updateComponent()
const value = this.get()
if( value ! = =this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
Copy the code
This value is the key of the watch object, such as name and obj.age:
watch: {
name() {
},
'obj.age': (a)= >{}}Copy the code
If the change is made, or an object is observed, or the deep argument is passed, and the user Watcher is called, the callback is executed.
Now, let’s go back to queueWathcer and see how this code works:
} else {
// When will I walk here?
For example, when executing a watch, assigning to reactive data triggers an update to another watch
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
Copy the code
When we enter else, another Watcher is triggered when the queue is refreshed. For example, when a watch method is executed, an assignment to reactive data triggers an update to another Watcher. Let’s take a look at this process through the following example:
<div id="demo">
{{name}} {{age}}
</div>
<script>
// Component Watcher id is 3
const app = new Vue({
el: '#demo',
data() {
return {
name: 'foo'.age: 18,}},watch: {
name() {
// The Watcher id is 1
this.age = 19
},
age() {
// The Watcher id is 2
console.log('age')
},
},
mounted() {
this.name = 'a'}})</script>
Copy the code
In Mounted, the Watcher of name and component rendering Watcher will be added. Then, on the “next frame”, the Watcher will be executed. According to our analysis, the Watcher of name will be executed first, and this. Age will be assigned. And the queue is currently being refreshed, so it goes to:
} else {
// When will I walk here?
For example, when executing a watch, assigning to reactive data triggers an update to another watch
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
Copy the code
Because the Watcher ID of age is smaller than the id of the component rendering Wathcer, the Watcher is inserted into the current queue.
Now, it’s time to look at nextTick:
import {noop} from 'shared/util'
import {handleError} from './error'
import {isIE, isIOS, isNative} from './env'
export let isUsingMicroTask = false
// Array of callback functions
const callbacks = []
let pending = false
// Refresh the callback array
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
// Traverse and execute
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = (a)= > {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeofMutationObserver ! = ='undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')) {// Use MutationObserver where native Promise is not available,
PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true,
})
timerFunc = (a)= > {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
timerFunc = (a)= > {
setImmediate(flushCallbacks)
}
} else {
timerFunc = (a)= > {
setTimeout(flushCallbacks, 0)}}// Put the cb function at the end of the callback queue
export function nextTick(cb? : Function, ctx? : Object) {
let _resolve
callbacks.push((a)= > {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')}}else if (_resolve) {
_resolve(ctx)
}
})
if(! pending) { pending =true
// Execute the function asynchronously
timerFunc()
}
// $flow-disable-line
if(! cb &&typeof Promise! = ='undefined') {
return new Promise((resolve) = > {
_resolve = resolve
})
}
}
Copy the code
This file first goes through a series of feature judgments to determine which API to use to implement asynchro, and finally provides the timerFunc method for nextTick to call. NextTick will place the incoming callback into the callbacks, and the first time it is called because pending is false, timerFunc will be executed to start a macro/micro task, and the final flushCallbacks method will be executed in the future. This method is to execute all callbacks and reset pending to false.
At this point, asynchronous update logic analysis is about enough. Now, let’s answer the original question:
Title: The following print order? Name = ‘b’
<template>
<div id="app">
{{name}}
</div>
</template>
<script>
export default {
data() {
return {
name: 'a'
}
},
mounted() {
this.name = 'b'
Promise.resolve().then(() => {
console.log(1)
})
this.$nextTick(() => {
console.log(2)
})
}
};
</script>
Copy the code
The first question. Because this.name = ‘b’ triggers an update to Watcher, it starts an asynchronous task, which is implemented in the latest Chrome browser using microtasks (let’s call them task1). The promise.resolve (). Then callback is also placed in the microtask queue after task1. When this.$nextTick is executed, the callbacks will be placed on the callbacks, but it will still be executed within task1. So the order of print is 2, 1.
The second asked. The promise.resolve (). Then callback will be placed in the microtask queue, and this.$nextTick will start a microtask at the end of the microtask queue. So the next time the microtask queue is emptied, the order of printing will be 1, 2.