preface

NextTick is a well-known tool function in Vue, and we often use it in practice. So what functions does it actually have, how is it designed in Vue, and what scenarios can we learn from in daily life?

We use the latest Vue version of V2.6.14 to analyze, link to github.com/vuejs/vue/b…

The text analysis

What

What nextTick is, refer to the official Vue 2 API documentation: cn.vuejs.org/v2/api/#Vue…

It can be called a callback function, which we can call a task, and that’s already documented in Vue. Execute this task (callback) at the end of the next DOM update loop so that you can retrieve the updated DOM.

How

Let’s take a look at the entire nextTick code, removing the flow reference and adding our own comments:

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
 
// Whether MicroTask is used, such as Promise MutationObserver
// MacroTask setImmediate setTimeout is used if the browser does not support it
// See the browser eventLoop article for more information
export let isUsingMicroTask = false
// Store all callback queues, which can be thought of as individual tasks
const callbacks = []
// Whether to wait for execution
let pending = false
// Execute the task queue in sequence, loop & execute
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
 
// Implement asynchronous function, from the name of the next tick, namely a timer
let timerFunc
 
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  // Native Promise async
  const p = Promise.resolve()
  timerFunc = () = > {
    // Using the promise.then implementation, a micro task is followed by the flushCallbacks
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Downgrade to use MutationObserver
  // 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 = () = > {
    // Trigger textNode changes, which in turn trigger the MutationObserver callback to execute flushCallbacks
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () = > {
    // Use setImmediate directly
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () = > {
    // The classic setTimeout
    setTimeout(flushCallbacks, 0)}}/ / the main implementation
export function nextTick (cb, ctx) {
  let _resolve
  // Add tasks one by one to the Callbacks queue
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  // If not waiting, the last callbacks task queue has completed
  // Then enter the wait state, re-enter the new round of waiting for the next timer and execute the new round of saved callbacks task queue
  if(! pending) { pending =true
    timerFunc()
  }
  NextTick ().then()
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

As you can see, there’s not a lot of code, but there’s a lot of handling, a lot of compatibility handling. The core implementation of nextTick is to take a queue and store all the tasks that need to be executed, and the nextTick (asynchronously) executes those tasks.

With this core implementation, we can implement a minimalist version of nextTick, regardless of compatibility and exceptions:

let pending = false
const tasks = []
const flushCallbacks = () = > {
  pending = false
  tasks.forEach(task= > task())
  tasks.length = 0
}
 
const p = Promise.resolve()
const timerFunc = () = > {
  p.then(flushCallbacks)
}
 
function nextTick(task) {
  tasks.push(task)
  if(! pending) { pending =true
    timerFunc()
  }
}
Copy the code

It’s only 20 lines, but it’s so core and powerful that we can use it like this:

const task1 = () = > console.log('1')
const task2 = () = > console.log('2')
 
console.log('before')
nextTick(task1)
nextTick(task2)
console.log('after')
 
Before after 1 2
Copy the code

At this point, you have a better idea of nextTick: collect tasks that need to be executed asynchronously and execute them on the nextTick.

Why

So why nextTick? Can’t we just do those tasks? In Vue, the official website also gave you the answer, details cn.vuejs.org/v2/guide/re…

For better performance, DOM update operations are stored in an asynchronous update queue, and DOM update operations are performed in the next tick.

Imagine if Vue had to update the DOM operation every time we updated the data, because our daily processing logic must look something like this:

const data = {
  title: 'hello'.desc: 'world'
}
this.msg = data.title
this.context = data.desc
Copy the code

This is still a partial scenario, not to mention the DOM update for our entire Vue application.

So Vue uses asynchronous update queues for optimization, which is the core thing that nextTick does in our analysis above.

conclusion

What can we learn or learn more about nextTick?

The queue

See that the operations on the queue (simulated by arrays, of course, are essentially the same) : add tasks to the queue, execute tasks in the queue, and empty the queue.

Queues are a very common data structure. As mentioned above, EventLoop is essentially the same as nextTick, but it has become more complex. There are multiple queues that need to be handled.

asynchronous

We know part of the implementation of timerFunc. Accordingly, we need to know which API operations are asynchronous, which kinds of asynchronous processing (MacroTask and MicroTask), what are the differences between them and the impact of their use, and how to choose when we encounter asynchronous scenarios.

There is also a point where the degraded scheme setTimeout is used. The second parameter passed is 0, so what is the effect of this time? What other uses can setTimeout have? How many parameters can setTimeout have? What type of return value can setTimeout have?

As an extension of the eventLoop lore, you need to distinguish between the browser environment and node.js environment.

Asynchrony and queue collide and can have a lot of sparks.

There are many times when we need to deal with asynchronous tasks, and for these tasks, the most appropriate data structure is the queue, such as the famous async library github.com/caolan/asyn… Is simply the asynchronous play to the extreme, there are a lot of good implementation ideas and skills, interested in can also be in-depth understanding.

Our realistic requirements are the same. For example, in the small program scenario, no more than 10 concurrent requests can be made, and any more requests will be cancelled. Therefore, we need to encapsulate a layer of requests, which in MPX is encapsulated as mX-Fetch, and we also require high and low priority requests. We need to implement our requirements with the help of queues.

An array of circulation

In flushCallbacks, we saw a trick that in our own simple implementation would directly facilitate the callbacks and then execute, whereas in Vue it would copy a new one and then execute it in a loop.

The reason for doing this is to consider the special case that if a callback is executed and nextTick is called again to update the Callbacks, then the execution is not expected. So we need to first copy the original, even in cb update callbacks will not affect our loop and execution, as expected.

This is a very rigorous place, we should also have this kind of thinking and awareness in the actual scene.

And there are a lot of extensions to this question, for array loops, what’s the difference between a forward loop and a reverse loop? Are they the same? And how different would it be if we used forEach with the for loop versus the array itself; And for the end of the loop, we say I < array.length and const len = array.length; What difference does it make if I < len?

Promise

Promise is a great thing, a very useful thing, and we need to understand it and use it. One of the more interesting points here is the return value handling of nextTick, which applies a trick: how the external state updates the Promise, as you see in the _resolve variable.

Promise, a major factory is basically in the investigation, Promise has what specifications, contains what definition, what API, how to realize a Promise.

I hope you will study and understand it in depth and master it. Promise!

Other small Tips

  • isNativeHow does he judge
  • Pending, heavy
  • How to handle errors
  • How to consider compatibility and degradation

The team number of Didi front-end technology team has been online, and we have synchronized certain recruitment information. We will continue to add more positions, and interested students can chat with us.