background
A recent project encountered such a problem, pseudo-code (VUE version 2.6.x) is as follows:
<template> <div class="layout"> <TopBar /> <div class="main" v-if="isRouterAlive"> <slot /> </div> </div> </template> <script> export default { async created() { this.admin = await ajax('... ') if (! admin) { this.$router.replace('/403') } this.$nextTick(() => { this.isRouterAlive = true }) } } </script>Copy the code
When no permissions are available, the ideal situation looks like this:
- Visit the home page
- Call the interface to get user permissions, which are false
- Route redirects to 403
this.isRouterAlive = true
Page 403 is displayed
Here’s what actually happened:
- Visit the home page
- Call the interface to get user permissions, which are false
this.isRouterAlive = true
, display the home page- The route goes to 403, and page 403 is displayed
$router. Replace (‘/403’) = await this.$router. Replace (‘/403’) = await this.
Analysis of the
This.$router. Replace and this.$nextTick.
Now we know that this.$router. Replace returns a promise, The timeFunc implementation priority in this.$nextTick is Promise –> MutationObserver –> setImmediate –> setTimeout, So in the browser environment this.$nextTick is also implemented based on a Promise, so let’s change the code:
/ / in case 1
async created() {
this.$router.replace('/ 403').then((a)= > {
console.log(1)})this.$nextTick((a)= > {
console.log(2)
this.isRouterAlive = true})}/ / 2
/ / 1
Copy the code
The console prints a 2, then a 1. Why would nextTick print a Promise first?
If this.$router.replace is a nested Promise, then of 1 is followed by a hidden THEN.
function myReplace () {
return new Promise((resolve) = > {
Promise.resolve().then((a)= > {
resolve()
})
})
}
async created() {
myReplace('/ 403').then((a)= > {
console.log(1)})Promise.resolve().then((a)= > {
console.log(2)})}/ / 2
/ / 1
Copy the code
Is it really something like this? Let’s modify the code again:
/ / case 2
async created() {
this.$router.replace('/ 403').then((a)= > {
console.log(1)
})
setTimeout((a)= > {
console.log(3)})this.$nextTick((a)= > {
console.log(2)
this.isRouterAlive = true})}/ / 2
/ / 3
/ / 1
Copy the code
Gee, why is it 3 to 1? This.$router. Replace returns a Promise. Isn’t Promise a microtask? Isn’t setTimeout a macro task?
This.$router. Replace returns a Promise, but if only the Promise is resolved, then 1 is added to the microtask queue and executed. This.$router. Replace executes a macro task like setTimeout, then resolve:
function myReplace () {
return new Promise((resolve) = > {
setTimeout((a)= > {
resolve()
}, 10) // Delay some time})}async created() {
myReplace('/ 403').then((a)= > {
console.log(1)
})
setTimeout((a)= > {
console.log(3)})Promise.resolve().then((a)= > {
console.log(2)})}/ / 2
/ / 3
/ / 1
Copy the code
So the question is,this.$router.replace
What exactly are the macro tasks that are executed internally?
No way, can only interrupt the point to see the source code debugging, the specific function call stack here is not described. Route 403 is defined in the project router.ts as:
{
path: '/ 403'.name: '403'.component: (a)= > import('./views/403.vue')}Copy the code
The this.$router.replace function takes a path argument, which in our example is ‘/403’. The path argument is used to find a matching RouteRecord. Import (‘./views/403.vue’) from resolveAsyncComponents (‘./views/403.vue’) Then route redirection and view update are performed.
Dynamic imports are also based on promises:
function myReplace () {
return new Promise((resolve) = > {
import('./views/403.vue').then((a)= > {
resolve()
})
})
}
async created() {
myReplace('/ 403').then((a)= > {
console.log(1)
})
setTimeout((a)= > {
console.log(3)})Promise.resolve().then((a)= > {
console.log(2)})}/ / 2
/ / 3
/ / 1
Copy the code
Therefore, the macro task performed inside this.$router.replace is in import.
So again, the question is,import
What exactly are the macro tasks that are executed internally?
Dynamic import accepts the url of the module as a parameter, so it is not hard to guess that the module needs to be requested to load inside import, so the macro task performed inside import is to load the HTTP request of the module, namely:
function myReplace () {
return new Promise((resolve) = > {
return new Promise((resolve= > {
ajax('./views/403.vue').then((a)= > {
resolve()
})
}))
})
}
async created() {
myReplace('/ 403').then((a)= > {
console.log(1)
})
setTimeout((a)= > {
console.log(3)})Promise.resolve().then((a)= > {
console.log(2)})}/ / 2
/ / 3
/ / 1
Copy the code
At this point, it is not difficult to go back to the background of the problem:
async created() {
this.admin = await ajax('... ')
if(! admin) {this.$router.replace('/ 403')}this.$nextTick((a)= > {
this.isRouterAlive = true})}Copy the code
- Visit the home page
- Call the interface to get user permissions, which are false
- perform
this.$router.replace('/403')
Trigger a macro task that asynchronously loads module 403 - perform
this.$nextTick
Add a microtask to the microtask queue - Execute the microtask queue,
this.isRouterAlive = true
, display the home page - Loading 403 Asynchronously After the module is loaded, the route is redirected to 403, and page 403 is displayed
Is this the end of it? Far from it!
Let’s change the code again:
/ / case 3
async created() {
this.$router.replace('/ 403').then((a)= > {
console.log(1)})Promise.resolve().then((a)= > {
console.log(2)})this.$nextTick((a)= > {
console.log(3)})}Copy the code
When the 403 module is loaded asynchronously, it is not hard to see from the previous analysis that the executing code outputs 2, 3, and 1 in sequence.
** But what about when the 403 module isn’t loaded asynchronously? Route 403 is defined in the project router.ts as:
{
path: '/ 403'.name: '403'.component: Page403 // Page403 = import Page403 from './views/403.vue'
}
Copy the code
This.$router.replace does not need to import from this.$router.replace.
function myReplace () {
return new Promise((resolve) = > {
resolve()
})
}
async created() {
myReplace('/ 403').then((a)= > {
console.log(1)})Promise.resolve().then((a)= > {
console.log(2)})Promise.resolve().then((a)= > {
console.log(3)})}Copy the code
Therefore, when the 403 module is loaded synchronously, the executing code should output 1, 2, and 3 in sequence, according to the previous analysis.
But the actual code is executed in the order of 3, 1, and 2.
Gee, why did it print 3 first? Why was the callback in this.$nextTick executed first?
Let’s take a look at the nextTick source code, and I’ve simplified it:
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc = Promise.resolve().then(flushCallbacks)
export function nextTick (cb? : Function) {
callbacks.push(cb)
if(! pending) { pending =true
timerFunc()
}
}
Copy the code
A global callbacks array is maintained in nextTick, and nextTick is called for the first time:
- Put the callback function in
callbacks
中 pending
为false
, the implementation oftimeFunc
, add microtasksflushCallbacks
To the microtask queue
A subsequent call to nextTick from the same tick simply puts the callback function into the Callbacks and does not trigger a new microtask. Therefore, callbacks that call nextTick multiple times in the same tick will eventually be executed by the microtask flushCallbacks that were added when nextTick was first called.
Analyze the following code:
/ / case 4
async created() {
this.$nextTick((a)= > {
console.log(1)})Promise.resolve().then((a)= > {
console.log(2)})this.$nextTick((a)= > {
console.log(3)})}/ / 1
/ / 3
/ / 2
Copy the code
- First call
nextTick
, add microtasksflushCallbacks
To the microtask queuecallbacks
There is a callback function that outputs 1 - perform
Promise.resolve()
To add a microtask with output 2 to the microtask queue - Second call
nextTick
To add the callback function to output 3callbacks
In the array - Execute the microtask queue, execute the first microtask
flushCallbacks
, that is, execute in sequencecallbacks
Array, printing 1,3 in sequence - Execute the second microtask, output 2
$router. Replace (‘/403’); this.$router. Replace (‘/403’); FlushCallbacks are added to the front of the microtask queue, so 3 is printed first, i.e. when the 403 module is loaded synchronously, the code in case 3 is equivalent to:
function syncReplace () {
return new Promise((resolve) = > {
this.$nextTick((a)= > {})
resolve()
})
}
async created() {
syncReplace('/ 403').then((a)= > {
console.log(1)})Promise.resolve().then((a)= > {
console.log(2)})this.$nextTick((a)= > {
console.log(3)})}Copy the code
So the question is,this.$router.replace('/403')
Why is it called internallynextTick
?
Let’s hit a breakpoint and look at the function call stack:
As shown above, when the 403 module is loaded synchronously, the route is updated synchronously when this.$router.replace(‘/403’) is executed. The update procedure calls queueWatcher. QueueWatcher calls nextTick internally (this.$router.replace(‘/403’) actually triggers multiple updates (watcher) when the 403 module is loaded synchronously, and nextTick is executed multiple times, I won’t go into that here.
At this point, let’s look at the execution of case 3 (when the 403 module is loaded synchronously) :
/ / case 3
async created() {
this.$router.replace('/ 403').then((a)= > {
console.log(1)})Promise.resolve().then((a)= > {
console.log(2)})this.$nextTick((a)= > {
console.log(3)})}Copy the code
- perform
this.$router.replace('/403')
, route update triggers route correlationwatcher
, the first callnextTick
, add microtasksflushCallbacks
To the microtask queue - Add the microtask with output 1 to the microtask queue
- perform
Promise.resolve()
To add a microtask with output 2 to the microtask queue - perform
nextTick
To add the callback function for output 3 to the microtaskflushCallbacks
thecallbacks
In the array - Execute the microtask queue, execute the first microtask
flushCallbacks
, that is, execute in sequencecallbacks
Array callback function, output 3 - Execute the second microtask, output 1
- Execute the second microtask, output 2
thinking
Consider the output order of the following code.
Question 1:
<template> <div class="app"> {{msg}} </div> </template> <script> export default { data() { return { msg: 'aaa' } }, created() { this.msg = 'bbb' Promise.resolve().then(() => { console.log(1) }) this.$nextTick(() => { console.log(2) }) } } </script>Copy the code
Question 2:
<template> <div class="app"> {{msg}} </div> </template> <script> export default { data() { return { msg: 'aaa' } }, mounted() { this.msg = 'bbb' Promise.resolve().then(() => { console.log(1) }) this.$nextTick(() => { console.log(2) }) } } </script>Copy the code