Is the so-called whetstone is not wrong to cut wood workers, before really reading the source code to understand some variables is how to judge, the role of pure function is very necessary. Know what hammers, axes, wrenches, and scissors are for so you don’t feel unprepared when you meet them for the first time.

NextTick is a core implementation of VUE and one of the commonly used methods in development. The common usage methods are as follows:

this.$nextTick(() = > {
	console.log('CALL back after DOM update')})Copy the code

Defer the callback until after the next DOM update cycle. Before reading this article, make sure you understand the JS event loop mechanism.

This method is registered in renderMixin(Vue). Follow the core/instance/index.js directory to render

export function renderMixin (Vue: Class<Component>) {
  // Other code ignored
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)}// Other code ignored
}
Copy the code

We see a $nextTick function hanging from the Vue prototype chain, with only one sentence pointing to the nextTick function, the first argument being the function to call back, and the second argument being the current instance object. The nextTick function is defined in the current directory: core/util/next-tick.js

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

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
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () = > {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

Let’s not be intimidated by this code, but it all boils down to one sentence. Only the framework does some boundary processing:

// When the developer executes
this.$nextTick(() = > {
	console.log('CALL back after DOM update')})// Execute
Promise.resolve().then(() = > {
	console.log('CALL back after DOM update')})Copy the code

There are many ways to register an asynchronous task, from lines 19 to 50. The most common way to register an asynchronous task is to use Promise, MutationObserver, setImmediate, and setTimeout. Which method to use is chosen in order of preference based on the current host environment’s support for asynchronous methods. Let’s use Promise as an example:

let timerFunc
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
}
Copy the code

As long as the current host environment supports Promise, this code essentially defines a function, timerFunc

let timerFunc = () = > {
	Promise.resolve().then(flushCallbacks);
	if (isIOS) setTimeout(noop)
}
Copy the code

There is such a code if (isIOS) setTimeout(noop) and the main logic does not seem to match, actually this is to solve some very strange problems in UIWebViews, we boil it down to the boundary problem handling, we will not discuss in detail here. So what is flushCallbacks?

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]()
  }
}
Copy the code

FlushCallbacks is clearly a function that loops through copies of a number of callbacks, executes calls to each item separately, and flushes the callbacks array.

Let’s look at the nextTick main function:

export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

There are some boundary issues, such as how to handle exceptions when a developer calls this.$nextTick() and does not return callback parameters. It also contains a try… catch… If the execution is abnormal, an error message is displayed. The main logic is these two things:

  • 1, arraycallbacksAdd a function, and the core of the function iscb.call(ctx), that is, to execute the callback function passed by the developer, scoped to the current component instance;
  • 2, performtimerFuncThe delta function, which is thetaPromise.resolve().then(flushCallbacks).

The pending variable runs through flushCallbacks and nextTick. It is a switch whose true or false indicates whether the callback queue is waiting for a refresh. An initial value of false indicates that the callback queue is empty and does not need to be refreshed. So why the pending switch and why the callbacks are stored in an array? The answer is to save performance, for example by calling the nextTick method three times in a row

methodFn () {
  this.$nextTick(() = > { console.log(1)})this.$nextTick(() = > { console.log(2)})this.$nextTick(() = > { console.log(3)})}Copy the code

In this example, the nextTick method is called three times, but only the first time the ‘timerFunc’ function is executed to register ‘flushCallbacks’ as a’ microtask ‘. At that time, the ‘flushCallbacks’ function is not executed. Because it waits for the next two nextTick methods to register ‘flushCallbacks’ as’ microtasks’ at timerFunc, the’ flushCallbacks’ function doesn’t execute at that time. Because it will wait for the next two nextTick methods to register ‘flushCallbacks’ as’ microtasks’ with’ timerFunc, ‘which doesn’t execute at that point, Because it waits for the next two calls to the nextTick method to execute, or, more precisely, until the call stack is cleared. At the end of the nextTick cycle, three functions are pushed in the callbacks array to be called back. The microtask ‘flushCallbacks’ will be executed when the current stack terminates.