How to implement nextTick in Vue, and how to obtain the node after DOM update, with this question we take a look at the Vue source code is how to implement?

directory

  • The role of nextTick
  • Vue data update operation
  • Implementation principle of nextTick

The role of nextTick

When we update the state of the data, we want to be able to get the latest node to do some of our callback operations, but the problem is that we usually can’t get the updated DOM because we haven’t rendered it yet, so we need to use this method.

We see a lot of talk about executing after the next DOM update cycle, but when does that cycle refer to?

In VUE, our watcher will know about the data changes, and it will perform notify operation, patch operation for comparison and a series of processes, and finally perform render function to refresh the page.

However, Watcher does not trigger the rendering process synchronously. It maintains a queue to which Watcher is pushed when it needs to render and only triggers the rendering process in the next event loop.

Why is vUE using asynchronous update queues?

We know that the minimum granularity of a VUE update is a component, and if we change the same data multiple times in the same event cycle, we will inevitably trigger multiple render processes, which will waste performance.

Instead of rendering multiple times, Vue puts the information watcher receives into a queue, and the next time it adds the same watcher, it doesn’t push it.

When a new watcher does not already exist in the queue, it is added. The render process is triggered in turn and the queue is emptied in the next event loop. Guarantee that even if two data changes in the same event loop will trigger a render.

Event loop mechanism

We all know that JavaScript is single-threaded, so there is only one main thread to handle all the tasks. When an asynchronous task such as a timer is encountered, the task is suspended and the callback function is executed according to certain rules.

The event loop layers synchronous and asynchronous tasks, which in turn are divided into macro and micro tasks. When there are no more tasks in the stack, it goes to the microtask queue and executes the microtask callback function until it is empty.

Next, go to the macro task queue and pull out a task’s callback stack, and then check the microtask when the stack is finished. An infinite loop constitutes an event loop.

Macro task

  • setTimeout
  • setInterval
  • setImeediate
  • I/O
  • UI interaction events

Micro tasks

  • Promise.then
  • MutationObserver
  • process.nextTick

Vue data update operation

We know that vue performs DOM updates asynchronously by default, and when we set the data, Watcher will know where to rely on this property and push watcher to the asynchronous queue.

When a reactive data changes, its setter function notifies the Dep in the closure, and the Dep calls all the Watch objects it manages. Trigger the update implementation of the Watch object.

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 on next tick. * /
        queueWatcher(this)}}Copy the code

The queueWatcher function is called when an update is performed asynchronously.

In queueWatcher’s implementation, instead of updating the view immediately, the Watch object is pushed into a queue, which is in a waiting state. In queueWatcher’s implementation, the Watch object will continue to be pushed into the queue, and when the next tick runs, These Watch objects are then iterated out to update the view. Also, a Watcher with a duplicate ID is not added to the queue more than once.

 /* 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) {
  /* Get watcher's id*/
  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.
      let i = queue.length - 1
      while (i >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1.0, watcher)
    }
    // queue the flush
    if(! waiting) { waiting =true
      nextTick(flushSchedulerQueue)
    }
  }
}

Copy the code

We see that nextTick is finally executed above, which is to push a function into a microTask or task and execute the function passed in by nextTick after the current stack finishes executing (and maybe some other tasks that need to be executed at the top).

I understand that the data update callbacks are fetched from the microtask queue, and then we manually call $nextTick to append the callback we passed to the callbacks array. No matter how many microtasks you have in front of you, the next one you’re going to execute will be the one we passed in.

So we can get the data after the DOM update.

Implementation principle of nextTick

We see three important variables defined in nextTick in the source code:

src/core/util/next-tick.js

  • Callbacks: Store all callbacks that need to be executed
  • Pending: Indicates whether a callback function is being executed
  • TimerFunc: Used to trigger the execution of the callback function

First we saw that nextTick passed in two parameters, a CB callback and a CTX context.

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()
  }
  // If no callback function is provided, a Promise object is returned
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code
  • 1. Get the callback function push into the Callbacks array
  • If the pending state is not pending, the timerFunc function is executed

So what exactly is timerFunc?

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

And we see that we’re assigning different values to it in different cases.

  • The first step is to determine whether the Promise is supported, and if it is, execute the flushCallbacks asynchronously in the Promise, and the tag uses microtasks.

  • If promise is not supported, the MutationObserver method is supported. If promise is not supported, the MutationObserver class is new, and a text node is created to listen for changes in the data. The flushCallbacks method is executed asynchronously

  • SetImmediate Implements flushCallbacks asynchronously if setImmediate is supported

  • If none of the above methods is supported, use setTimeout) and execute the flushCallbacks method asynchronously

FlushCallbacks method

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

The flushCallbacks method is the function that executes the pass loop. Notice here we see that it copies a set of numbers, what does it do?

This is to prevent nextTick from being added to the Callbacks during execution, preventing loops.