usage

Vue.nextTick( [callback, context] )

A deferred callback is performed after the next DOM update loop ends. Use this method immediately after modifying the data to get the updated DOM.

// Modify the data
vm.msg = 'Hello'
// DOM has not been updated yet
Vue.nextTick(function () {
  // DOM is updated
})

// Used as a Promise
Vue.nextTick()
  .then(function () {
    // DOM is updated
  })
Copy the code

New since 2.1.0: Returns a Promise if no callback is provided and in an environment that supports Promise. Note that Vue doesn’t come with Promise’s polyfill, so if your target browser doesn’t support Promise nave, you’ll have to provide polyfill yourself.

The vue. nextTick method works the same as the vm.$nextTick method, except that the this callback is automatically bound to the instance calling it.

Why does vue.js use asynchronous update queues

The Vue implementation is responsive, not that the DOM changes immediately after the data changes, but that the DOM is updated according to a certain strategy.

In vue.js, Watcher is notified when the state changes and triggers the rendering process of the virtual DOM. Note that Watcher does not trigger the render operation synchronously, but asynchronously.

If two data changes in the same event cycle, the component’s Watcher gets two notifications, rendering twice. However, there is no need to render twice; the virtual DOM renders the entire component. We just need to render the DOM of the entire component up to date once all the states have been modified.

Vue.js solves this problem by adding the notified Watcher instance to the queue and caching it, and checking to see if the same Watcher already exists before adding it to the queue. If not, the watcher is added to the queue. During the next event loop, vue.js causes the watcher in the queue to trigger the rendering process and empty the queue.

To put it simply, Vue does not update views immediately after data changes are made, but instead updates views after all data changes in the same event loop are complete.

Event loop

Recommended reading: juejin.cn/post/693211…

After all tasks in the execution stack are completed, it checks whether there are any events in the microtask queue. If there are, it calls the corresponding callback of the events in the microtask queue once until it is empty. Then it fetches an event from the macro task queue and adds the corresponding callback to the execution stack. After all tasks in the execution stack are executed, it continues to check whether there are any events in the microtask queue. Repeating this process creates an infinite loop called an event loop.

Macrotask: setTimeout, setInterval, setImmediate, I/O, UI Rendering

Microtasks: Promise.then, Process.nexttick, MutationObserver, queneMicrotask(start a microtask)

Execution stack

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

If there are more functions, there will be multiple function execution contexts, and a new execution context will be created each time the function is called. The JS engine creates an execution context stack to manage the execution context. The execution context stack can be thought of as a stack structure for storing function calls, following the principle of first in, last out.

If the above two concepts are still very vague, it is suggested to see the foundation again first, I pretend you all understand, next to see ~

Realize the principle of

Vue is executed 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.

For example, when you set vm.someData = ‘new value’, the component does not immediately re-render. When the queue is refreshed, the component is updated in the next event loop “TICK”. In most cases we don’t need to worry about this process, but if you want to do something based on the updated DOM state, it can be tricky. While vue.js generally encourages developers to think in a “data-driven” way and avoid direct contact with the DOM, sometimes we have to. To wait for Vue to finish updating the DOM after the data changes, use vue.nexttick (callback) immediately after the data changes. This callback will be called after the DOM update is complete.

<div id="example">{{message}}</div>
Copy the code
var vm = new Vue({
  el: '#example'.data: {
    message: '123'
  }
})
vm.message = 'new message' // Change the data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})
Copy the code

The complete code

Note: 2021.02.23 from github download down the source code, different versions of the implementation of a slight difference.

IsUsingMicroTask: Whether to add the callback to the macro task queue
export let isUsingMicroTask = false

// Store user registration callback
const callbacks = []
// Flags whether a task has been added to the task queue
let pending = false

// Execute all the functions in the callbacks one by one, then list the situations
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Encapsulates the function that adds the task to the task queue
let timerFunc

// Support promises and use promises, the highest priority in microtasks
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () = > {
    p.then(flushCallbacks)
	
    // noop is an empty function wrapped inside vue
    if (isIOS) setTimeout(noop)
  }
  
  isUsingMicroTask = true
  
  // If promises are not supported, use MutationObserver, which is called when the specified DOM changes
} 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
  SetImmediate Is used if MutationObserver is not supported, but this feature is only supported by the latest versions of IE and Node
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  timerFunc = () = > {
    setImmediate(flushCallbacks)
  }
} else {
  // Use setTimeout if none of the above is supported, then demote to macro task
  timerFunc = () = > {
    setTimeout(flushCallbacks, 0)}}export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)	// Execute a callback if there is one
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)		// Execute the promise's resolve if there is no callback but a promise is supported
      // The reason for doing this: The official documentation states that if no callback is provided and in an environment that supports promises, a Promise is returned}})// Check that there are no tasks in the task queue
  if(! pending) { pending =true
    timerFunc()		// Add the task to the task queue
  }
  
  _resolve is resolve when there is no callback but promise is supported
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}
Copy the code

Little doubt

Why consider setImmediate first and then setTimeout last?

In each rotation check, the priorities of observers are as follows: Idle observer > I/O Observer > Check observer.

Idle Observer: process.nexttick

I/O Observer: General I/O callbacks, such as network, file, database I/O, etc

Check Observer: setTimeout>setImmediate

Shouldn’t setTimeout have a higher priority? !

In HTML5, the minimum interval of setTimeout is 4ms, which means that 0 is actually not set to the minimum of 4ms by default. We increase this delay to a point where setImmediate executes before setTimeout, because the setTimeout timer has not yet arrived when the Macro-Task loop is entered.

In newer Versions of Node, Process. nextTick loops through setImmediate and then jumps out of the loop after running through all of the setImmediate. — Node.js

🌰

Let’s reinforce this with an example! (Embarrassing if it’s wrong = =)

<template>
    <div>
        <ul>
            <li class="example" v-for="item in list1">{{item}}</li>
        </ul>
        <ul>
            <li class="example" v-for="item in list2">{{item}}</li>
        </ul>
        <ol>
            <li class="example" v-for="item in list3">{{item}}</li>
        </ol>
        <ol>
            <li class="example" v-for="item in list4">{{item}}</li>
        </ol>
        <ol>
            <li class="example" v-for="item in list5">{{item}}</li>
        </ol>
    </div>
</template>
<script type="text/javascript">
export default {
    data() {
        return {
            list1: [].list2: [].list3: [].list4: [].list5: []}},created() {
        this.composeList12()
        this.composeList34()
        this.composeList5()
        this.$nextTick(function() {
            // DOM is updated
            console.log('finished test ' + new Date().toString(),document.querySelectorAll('.example').length)
        })
    },
    methods: {
        composeList12() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                this.$set(me.list1, i, 'I am a test message ~ ~ la la la ' + i)
            }
            console.log('finished list1 ' + new Date().toString(),document.querySelectorAll('.example').length)

            for (let i = 0; i < count; i++) {
                this.$set(me.list2, i, 'I am a test message ~ ~ la la la ' + i)
            }
            console.log('finished list2 ' + new Date().toString(),document.querySelectorAll('.example').length)

            this.$nextTick(function() {
                // DOM is updated
                console.log('finished tick1&2 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })
        },
        composeList34() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                this.$set(me.list3, i, 'I am a test message ~ ~ la la la ' + i)
            }
            console.log('finished list3 ' + new Date().toString(),document.querySelectorAll('.example').length)

            this.$nextTick(function() {
                // DOM is updated
                console.log('finished tick3 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })

            setTimeout(me.setTimeout1, 0)},setTimeout1() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                this.$set(me.list4, i, 'I am a test message ~ ~ la la la ' + i)
            }
            console.log('finished list4 ' + new Date().toString(),document.querySelectorAll('.example').length)

            me.$nextTick(function() {
                // DOM is updated
                console.log('finished tick4 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })
        },
        composeList5() {
            let me = this
            let count = 10000

            this.$nextTick(function() {
                // DOM is updated
                console.log('finished tick5-1 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })

            setTimeout(me.setTimeout2, 0)},setTimeout2() {
            let me = this
            let count = 10000

            for (let i = 0; i < count; i++) {
                this.$set(me.list5, i, 'I am a test message ~ ~ la la la ' + i)
            }
            console.log('finished list5 ' + new Date().toString(),document.querySelectorAll('.example').length)

            me.$nextTick(function() {
                // DOM is updated
                console.log('finished tick5 ' + new Date().toString(),document.querySelectorAll('.example').length)
            })
        }
    }
}
</script>
Copy the code

reference

Vue official document: cn.vuejs.org/v2/api/#Vue…

The principle of Vue. NextTick and use: segmentfault.com/a/119000001…