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:

  1. Visit the home page
  2. Call the interface to get user permissions, which are false
  3. Route redirects to 403
  4. this.isRouterAlive = truePage 403 is displayed

Here’s what actually happened:

  1. Visit the home page
  2. Call the interface to get user permissions, which are false
  3. this.isRouterAlive = true, display the home page
  4. 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.replaceWhat 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,importWhat 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
  1. Visit the home page
  2. Call the interface to get user permissions, which are false
  3. performthis.$router.replace('/403')Trigger a macro task that asynchronously loads module 403
  4. performthis.$nextTickAdd a microtask to the microtask queue
  5. Execute the microtask queue,this.isRouterAlive = true, display the home page
  6. 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:

  1. Put the callback function incallbacks
  2. pendingfalse, the implementation oftimeFunc, add microtasksflushCallbacksTo 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
  1. First callnextTick, add microtasksflushCallbacksTo the microtask queuecallbacksThere is a callback function that outputs 1
  2. performPromise.resolve()To add a microtask with output 2 to the microtask queue
  3. Second callnextTickTo add the callback function to output 3callbacksIn the array
  4. Execute the microtask queue, execute the first microtaskflushCallbacks, that is, execute in sequencecallbacksArray, printing 1,3 in sequence
  5. 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
  1. performthis.$router.replace('/403'), route update triggers route correlationwatcher, the first callnextTick, add microtasksflushCallbacksTo the microtask queue
  2. Add the microtask with output 1 to the microtask queue
  3. performPromise.resolve()To add a microtask with output 2 to the microtask queue
  4. performnextTickTo add the callback function for output 3 to the microtaskflushCallbacksthecallbacksIn the array
  5. Execute the microtask queue, execute the first microtaskflushCallbacks, that is, execute in sequencecallbacksArray callback function, output 3
  6. Execute the second microtask, output 1
  7. 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