concept

NextTick takes a callback function as an argument that delays the callback until after the next DOM update cycle, or returns a Promise if there is no callback and in an environment that supports promises

Application scenarios

When you update the data, you need to do something with the new DOM. In fact, the view doesn’t update immediately after the data is updated, so you can’t get the updated DOM because you need the nextTick method before you re-render it

.methods: {
    handle () {
        // Modify the data
        this.msg = 'change'
        this.$nextTick(() = > {
            // Dom updated, do something...}}})...Copy the code

Update process

In vue.js, Watcher is notified when the state changes and triggers the rendering process of the virtual DOM. This operation is asynchronous, so there is a task queue to which Watcher is pushed whenever rendering is required. Let Watcher trigger the rendering process again in the next event loop

Event loop

Js is a single-thread non-blocking language, which means that there will be a main thread to handle all tasks when executing JS code. Non-blocking means that when the code needs to handle asynchronous tasks, the main thread will suspend the task, and when the asynchronous task is finished, the main thread will execute the corresponding callback according to certain rules

There are two types of asynchronous tasks: macro tasks and micro tasks

When all tasks in the execution stack are completed, it checks whether there are any events in the microtask queue. If there are, the callback corresponding to the events in the microtask queue will be executed once until it is empty. Then fetch an event from the macro task queue and add the corresponding callback to the current execution stack. When all tasks in the execution stack are executed, check whether there is an event in the micro task queue. Repeating this process indefinitely creates an infinite loop: a loop of events

Common microtasks:

  • Promise.then
    Copy the code
  • MutationObserver
    Copy the code
  • Object.observe
    Copy the code
  • Process.nexttick (server side)Copy the code

Common macro tasks:

  • setTimeout
    Copy the code
  • setInterval
    Copy the code
  • setImmediate
    Copy the code
  • MessageChannel
    Copy the code
  • requestAnimationFrame
    Copy the code
  • I/O
    Copy the code
  • UI interaction eventsCopy the code

Perform the stack:

When a method is executed, JS generates an execution environment corresponding to the method, also known as the execution context. The execution environment has the private scope of the method, the pointer to the upper scope, method parameters, variables defined in the private scope, and the this object. The execution environment is added to an execution stack, which is called the execution stack

The $nextTick method on the VUE prototype simply calls the nextTick method

import { nextTick } from '.. /util/index'
Vue.prototype.$nextTick = function(fn) {
    return nextTick(fn, this)}Copy the code

Simple implementation

const callbacks = []
let pending = false
function flushCallbacks () {
  pending = false                           // Perform a callback to change the state back to initial
  let copies = callbacks.slice(0)           // shallowly copy the callback array
  callbacks.length = 0                      // Perform all callbacks at once, perform the empty callback
  for(let i = 0; i < copies.length; i++) {
    copies[i]()                             // Call all callbacks}}let p = Promise.resolve()
let microTimerFunc = () = > {                // Put the callback into the microtask
  p.then(flushCallbacks)
}

function nextTick (cb, ctx) {
  callbacks.push(() = > {
    if (cb) {
      cb.call(ctx)
    }
  })
  if(! pending){// An event loop that calls multiple NextTicks only once
    pending = true
    microTimerFunc()
  }
} 
Copy the code

The source code to achieve

In the vue source code, we added compatibility processing, a way to convert promises into macro tasks if the environment doesn’t support them, and a way to enforce the use of macro tasks.

const callbacks = []
let pending = false
// Perform the callback
function flushCallbacks () {
  pending = false
  let copies = callbacks.slice(0)
  callbacks.length = 0
  for(let i = 0; i < copies.length; i++){
    copies[i]()
  }
}
let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine which macro task the browser supports
if(typeofsetImmediate ! = ='undefined' && isNative(setImmediate)){
  macroTimerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} else if (typeofMessageChannel ! = ='undefined' && (
  isNative(MessageChannel) ||
  MessageChannel.toString === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () = > {
    port.postMessage(1)}}else {
  macroTimerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}// If Promise is not supported, use macro tasks directly
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () = > {
    p.then(flushCallbacks)
  }
} else {
  microTimerFunc = macroTimerFunc
}

export function withMacroTask (fn){
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null.arguments)
    useMacroTask = false
    return res
  })
}

export function nextTick (cb, ctx) {
  let _resolve
  callbacks.push(() = > {
    if (cb) {
      cb.call(ctx)
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending){ pending =true
    if (useMacroTask) { // Whether to use macro tasks
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // If there is no callback, return a promise
  if(! cb &&typeof Promise! = ='undefined') {return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}

// Whether the method is native
export function isNative (Ctor) {
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

Copy the code