Usage scenarios of $nextTick

Although Vue is data-driven, sometimes we have to manipulate the DOM to handle special scenarios, and Vue DOM updates are performed asynchronously, so we have to use $nextTick to retrieve the DOM asynchronously.

<template>
  <div>
    <span ref="msg">{{ msg }}</span>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: 'hello nextTick'}},methods: {
    changeMsg() {
      this.msg = 'hello world'
      console.log(this.$refs.msg.innerHTML, 'Synchronous fetch')
      this.$nextTick(() = > {
        console.log(this.$refs.msg.innerHTML, 'Asynchronous fetch')})}},mounted() {
    this.changeMsg()
  }
}
</script>
Copy the code

We can see that when we change the data directly, when we get the DOM, the value doesn’t change, but in $nextTick we can see that the data has changed. Why? Let’s take a look at the reason through the source code

Watcher view updated

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    /* Execute run to render the view directly
    this.run()
  } else {
    /* Asynchronously pushed to observer queue, called by scheduler. * /
    queueWatcher(this)}}Copy the code

If you have seen the reactive principle, there is an update function in Watcher that updates views. When this.sync is false, this indicates an asynchronous update, so queueWatcher is executed

 /* Push an observer object into the observer queue. If the same id already exists in the queue, the observer object will be skipped, unless it is pushed when the queue is refreshed
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  /* Check whether the id exists, if it already exists, skip directly, if it does not exist, mark the hash table has, for the next check */
  if (has[id] == null) {
    has[id] = true
    if(! flushing) {/* If not flushed, push to queue */
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      // If it is refreshed, it is removed from the queue and executed immediately
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1.0, watcher)
    }
    // queue the flush
    if(! waiting) {// Execute nextTick without waiting
      waiting = true

      if(process.env.NODE_ENV ! = ='production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
Copy the code

Through queueWatcher function, we can see, the Watcher is not immediately update the view, but will be placed in a queue, now is waiting waiting state, it will check whether the id is repeated, if repeat, will not put in the queue; The current Watcher cannot be refreshed. If it is refreshed, it is removed from the queue. The unrefreshed Watcher is queued. If there is no waiting, then the nextTick method is executed.

nextTick

With all that said, it’s finally time for nextTick

export let isUsingMicroTask = false // Whether microtasks are used

const callbacks = [] /* Stores asynchronously executed callback */
let pending = false /* a flag bit that does not need to be pushed again if timerFunc has already been pushed to the queue */

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we use (macro) tasks In combination with microtasks.
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc /* A pointer to a function that will be pushed to the task queue, and the timerFunc in the task queue will be called */

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    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) // In an isIOS environment, execute setTimeout
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && ( // MutationObserver has compatibility issues under IE
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 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 = () = > {
    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 = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  // Store cb with exception handling into the Callbacks array
  callbacks.push(() = > {
    if (cb) {
      try {
        / / call the cb ()
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    / / call
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    // Return the Promise object
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

NextTick takes two parameters, a callback function and the context of the current environment. Executing nextTick puts the callback function in the Callbacks callback queue, which is then executed via timerFunc. It then determines whether the current execution environment has a Promise, and if so, executes the contents of the callback via promise.resolve (). Then, or setTimeout if the environment is IOS. Because some versions of IOS don’t support Promise very well; If the current environment does not support Promises, downgrade to microtask MutationObserver. The comments also list many environments that do not support Promises, such as PhantomJS, iOS7, Android 4.4; If MutationObserver is not supported, then setImmediate is used. And the worst thing you can do is use setTimeout, so why not use setTimeout instead of setImmediate, is that setImmediate performs faster than setTimeout, Even if setTimeout is set to 0, there will still be a delay of 4 ms.

Why update views asynchronously

<template>
  <div>
    <div>{{value}}</div>
  </div>
</template>
Copy the code
export default {
    data () {
        return {
            value: 0
        };
    },
    mounted () {
      for(let i = 0; i < 1000; i++) {
        this.value++; }}}Copy the code

In mounted hooks, if the value is not updated asynchronously, the DOM will be updated every time the value ++ is added. With the asynchronous DOM queue, however, it only executes on the next tick, which ensures that I runs from 0 to 1000, greatly optimizing performance.