When working on projects, nextTick is often used. It is simply a setTimeout function that can be handled asynchronously. Replacing it with setTimeout seems to work, but is it that simple? So why don’t we just use setTimeout? Let’s dig deeper.

Found the problem

Remember there was a requirement to display a button that expands more depending on the number of lines of text, so we need to get the height of text after assigning data in Vue.

<div id="app"> <div class="msg"> {{msg}} </div> </div> new Vue({ el: '#app', data: function(){ return { msg: }}, mounted(){this.msg = console.log(document.querySelector('.msg'). //0}})Copy the code

No matter how you get it, the height of the text Div is 0; But getting it directly has value:

The same applies to passing parameters to child components; After we pass the parameters to the child component, we call a function in the child component to view the parameters.

<div id="app">
    <div class="msg">
        <form-report ref="child" :name="childName"></form-report>
    </div>
</div>
Vue.component('form-report', {
    props: ['name'].methods: {
        showName(){
            console.log('Subcomponent name:'+this.name)
        }
    },
    template: '<div>{{name}}</div>'
})
new Vue({
    el: '#app'.data: function(){
        return {
            childName: ' ',
        }
    },
    mounted(){
        this.childName = 'I am the child component name'
        this.$refs.child.showName()
    }
})
Copy the code

Although the name of the child component is displayed on the page, it is printed with an empty value:

Asynchronous update

We found that both of the above problems, whether the parent or the component, were caused by looking at the data immediately after assigning values to the data. Because the “View data” action is synchronous, and all after the assignment; So let’s assume that the assignment is an asynchronous operation and is not executed immediately. The Vue website describes the data operation as follows:

In case you haven’t noticed, Vue executes asynchronously when updating the DOM. As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop. If the same watcher is triggered more than once, it will only be pushed into the queue once. This removal of duplicate data while buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop, “TICK,” Vue refreshes the queue and performs the actual (de-duplicated) work. Vue internally attempts to use native Promise.then, MutationObserver, and setImmediate for asynchronous queues, and setTimeout(fn, 0) instead if the execution environment does not support it.

So when we set this. MSG = ‘some thing’, Vue does not immediately update the DOM data, but puts the operation in a queue; If we do it repeatedly, the queue will be de-duplicated; After all data changes in the same event loop are complete, the events in the queue are taken out for processing.

This is mostly to improve performance, because if you update the DOM in the main thread, you update the DOM 100 times in 100 cycles; However, if you update the DOM after the event loop completes, you only need to update it once. For those who are not familiar with event loops, see my other article to understand JS event loops from an interview question

To manipulate the DOM after the data update operation, we can use vue.nexttick (callback) immediately after the data changes; This callback will be called after the DOM update is complete to retrieve the latest DOM element.

// First demo
this.msg = 'I'm the test text'
this.$nextTick((a)= >{
    / / 20
    console.log(document.querySelector('.msg').offsetHeight)
})
// Second demo
this.childName = 'I am the child component name'
this.$nextTick((a)= >{
    // Subcomponent name: I am the subcomponent name
    this.$refs.child.showName()
})
Copy the code

NextTick source code analysis

Now that you know how nextTick works, let’s take a look at how Vue implements this wave of “operations.”

Vue pulled the nextTick source code into a single file, / SRC /core/util/next-tick.js, and deleted the comment about 60 or 70 lines. Let’s break it down.

const callbacks = []
let pending = false
let timerFunc

export function nextTick (cb? : Function, ctx? : Object) {
  let _resolve
  callbacks.push((a)= > {
    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 first go to where nextTick is defined and see what it does. You can see that it defines three variables in the outer layer, one of which is familiar from its name: callbacks, the queue we mentioned above; Defining variables in the outer layer of nextTick creates a closure, so every time we call $nextTick we’re adding a callback to the callbacks.

Pending indicates that the timerFunc function can only be executed once at a time. So what does this timerFunc function do? Let’s look at the code:

export let isUsingMicroTask = false
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  // Judgment 1: Whether to support Promises natively
  const p = Promise.resolve()
  timerFunc = (a)= > {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Judgment 2: Whether MutationObserver is supported natively
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = (a)= > {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  // Judgment 3: Whether setImmediate is native supported
  timerFunc = (a)= > {
    setImmediate(flushCallbacks)
  }
} else {
  // Judgment 4: None of the above, use setTimeout directly
  timerFunc = (a)= > {
    setTimeout(flushCallbacks, 0)}}Copy the code

There are several isNative functions, which are used to determine whether the passed parameters are supported natively in the current environment. For example, some browsers don’t support Promises, and isNative(Promise) returns false even though we used a shim (Polify).

Then, MutationObserver, and setImmediate Mediate, all of which do not support the use of setTimeout. The purpose of the flushCallbacks is to place the flushCallbacks function into either the microtask (judgments 1 and 2) or the macro task (judgments 3 and 4) and wait for the next event loop to execute. MutationObserver is a new feature in Html5 that listens for changes to the target DOM structure, i.e. the newly created textNode in the code. If it changes, the callback function in the MutationObserver constructor is executed, but it is executed in a microtask.

We finally found the final boss: flushCallbacks; NextTick is desperate to put it into microtasks or macros, but what exactly is it? Here’s a look at it:

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 are only eight lines long. What it does is very simple. It copies the Callbacks array, sets the callbacks to null, and executes each function in the copied array. So it’s only used to execute callbacks.

conclusion

Now that the entire nextTick code has been analyzed, the process can be summarized as follows:

  1. Put the callback into the callbacks to wait for execution
  2. Place the execution function in a microtask or macro task
  3. Events loop to the microtask or macro task, and the executing function in turn executes the callbacks in the Callbacks

Going back to the setTimeout we mentioned at the beginning, it can be seen that nextTick performs various compatibility processing on setTimeout, which can be broadly understood as putting callback functions into setTimeout for execution. SetTimeout is a macro task, so nextTick is usually executed before setTimeout. We can try it in the browser:

setTimeout((a)= >{
    console.log(1)},0)
this.$nextTick((a)= >{
    console.log(2)})this.$nextTick((a)= >{
    console.log(3)})// Run result 2 3 1
Copy the code

Finally, the conjecture is verified. After the current macro task is completed, the two micro tasks will be executed first, and then the macro task will be executed last.

For more front-end information, please pay attention to the public number [front-end reading].

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles