This is the 17th day of my participation in the August Text Challenge.More challenges in August
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. And when a watcher is triggered multiple times, it is only pushed into the queue once, and then all asynchronous tasks just pushed are called after the macro task of the current event loop ends.
Why are render updates performed asynchronously?
To improve performance, if you update the DOM in the main thread, you need to update the DOM 10 times in 10 cycles. With asynchronous queues, you only need to update once.
Vue updates the DOM principle
Description of data operation on Vue official website:
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 updates with the next “tick” when the event loop queue is empty. In most cases we don’t need to worry about this process, but if you want to do something after a DOM status update, 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 do. 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.
Vue.$nextTick()
Syntax: vue.nexttick ([callback, context])
Parameters:
{Function} [callback]
: callback function that provides a promise call when not passed{Object} [context]
: The context in which the callback function is executed. By default, it is automatically bound to the calling instance.
The Vue instance method vm.$nextTick is further encapsulated by setting the context parameter to the current Vue instance.
What it does: Executes the callback we put in after the DOM update
The DOM is not updated immediately after the data is updated. Therefore, defining the DOM directly will not work. Use the vue.$nextTick() function after the data changes. By placing operations on the DOM in the callback function of nextTick(), you can call the defined callback after the DOM is updated to complete operations on the updated DOM.
Case 1:
<template> <div class="hello"> <h2 class="myh2">{{test}}</h2> <button @click =" onSubmit"> <script> export default {name: 'HelloWorld', data() {return {test: 'hahahaha ',}}, methods: { onSubmit() { console.log('before:', document.querySelector('.myh2').innerHTML); This. test = 'console.log' (' after:', document.querySelector('.myh2').innerhtml); $nextTick() => {console.log('nextTick:', document.querySelector('.myh2').innerhtml); NextTick: hey hey hey hey}) console.log(' I'm sync code! '); //3. I am sync code! } } } </script>Copy the code
Set up a VUE project and write the above code in a VUE build.
As you can see, the initial value of test is’ hahahaha ‘, and the DOM displays’ hahahaha ‘; After clicking button, execute onSubmit(), and the first console.log prints ‘before: hahahaha ‘; Next, change the data and assign test to ‘hey, hey, hey, hey, hey’. At this point, the data has changed, but the synchronization code hasn’t finished yet, so the DOM is still ‘hahahaha’, so the second console.log prints ‘after: hahahaha ‘; This.$nextTick() executes the callback after the DOM update. Then execute the following sync code, printing ‘I’m sync code! ‘; After updating the DOM, execute the callback function of this.$nextTick () and print ‘nextTick: hey hey hey hey hey ‘.
As you can see from this example,this.$nextTick() simply puts the callback we put in there after the DOM update. How is this.$nextTick () implemented?
Vue.$nextTick(
NextTick’s implementation maintains it in a separate JS file, SRC /core/util/next-tick.js.
NextTick source code is divided into two parts:
- According to the
Ability to detect
, decide which way to execute the callback queue, andAssign the execution function of the callback queue (flushCallbacks) to timerFunc
. - Callbacks to the callback queue when $nextTick is called
New callback function
.Perform timerFunc ()
.
1. According toAbility to detect
, decide which way to execute the callback queue, andAssign the execution function of the callback queue (flushCallbacks) to timerFunc
.
Since macro tasks take more time than microtasks, microtasks are preferred when supported by browsers. If the browser does not support microtasks, use macro tasks.
Then, MutationObserver, and setImmediate, all of which do not support setTimeout at the end; 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.
Vue appends our own callback function to the MICROtask that updates the DOM to ensure that our code executes after the DOM update.
// Empty function, Import {noop} from 'shared/util' // error handler import {handleError} from './error' // Whether IE, IOS, built-in function import { IsIE, isIOS, isNative} from './env' Export let isUsingMicroTask = false // Used to store all callbacks that need to be executed const callbacks = [] // Let pending = false /** * does three things: Execute every function in the callbacks array (such as flushSchedulerQueue, the callback passed by the user when calling nextTick) */ Function flushCallbacks () {pending = false $nextTick ($nextTick, $nextTick, $nextTick, $nextTick, $nextTick, $nextTick); Avoid endless copies of const copies = callbcks.slice(0) callbacks.length = 0 for(let I = 0; i < copies.length; I ++) {copies[I]()}} let timerFunc {{copies[I]()}} let timerFunc {{copies[I]()} We use (macro) tasks (in combination with microtasks). However, there are subtle issues that can arise when the state changes before redrawing (e.g. #6813, out-of-in transition). // Also, using (macro) tasks in event handlers leads to some strange behavior // So we are now using microtasks everywhere again. // Decide 1: Whether to support Promise natively if(typeof Promise! == 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // IOS UIWebView, promise. then callbacks are pushed to the microTask queue, but the queue may not execute as expected // therefore, Add an empty timer to enforce microTask if(isIOS) setTimeout(noop)} isUsingMicroTask = true // Check 2: Whether MutationObserver is supported natively else if(! isIE && typeof MutationObserver ! == 'undefined' && (isNative(MutationObserver) || MutationObserver.toString === '[object MutationObserverConstructor]')) PhantomJS, iOS7, Android 4.4 let counter = 1 // Create a MO instance, FlushCallbacks const Observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: TimerFunc = (); // timerFunc = (); // timerFunc = (); => {counter = (counter + 1) % 2 textNode.data = String(counter)} isUsingMicroTask = true SetImmediate} else if (typeof setImmediate! () == 'undefined') {timerFunc = () => {setImmediate(flushCallbacks)} Else {timerFunc = () => {setTimeout(flushCallbacks, 0)}}Copy the code
2. Call $nextTick to the callbacks queueNew callback function
.Perform timerFunc ()
. :
/** * Accomplish two things: If pending is false, there is no flushCallbacks in the browser task queue. FlushCallbacks = "flushCallbacks";}} When flushCallbacks are executed, pending is set to false again. The next flushCallbacks function can enter the * pending task queue in the browser: At the same time, Only one flushCallbacks function in the browser's task queue * @param {*} cb receives a callback * @param {*} CTX context * @returns */ ———————————————— const callbacks = [] let pending = false export function nextTick(cb? Function, ctx: Callbacks. Push (() => {if(cb) {try {cb.call(CTX)} catch(e) { handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(CTX)}}) // Pending is false TimerFunc () has not been executed in this event loop. Pending indicates that timerFunc can only be executed once at a time. if(! Pending) {pending = true // timerFunc() in the browser task queue (preferred micro task queue)} Provide a promise-like call to nextTick().then(() => {}) // When _resolve is executed, then logic jumps to if(! cb && typeof Promise ! == 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }Copy the code
NextTick is exposed in the next-tick.js file, so when we call vue. nextTick or this.$nextTick we execute:
-
If cb is passed, press cb into the callbacks array.
-
If cb is not passed in, provide a promise-like call, such as nextTick().then(() => {}), which jumps to then logic when _resolve is executed.
_resolve = resolve; __resolve refers to the resolve function. When __resolve is executed, it is resolved and therefore jumps to hen logic.
-
Perform timerFunc ()
TimerFunc is the variable that stores the flushCallbacks function, so executing timerFunc() will execute the flushCallbacks function. The flushCallbacks function iterates through the callbacks and then executes the corresponding callback (if there is no callback, execute _resolve to enter the then logic).
The reason why callbacks do not execute callbacks directly on nextTick is to ensure that they execute nextTick multiple times and do not start multiple asynchronous tasks. Instead, these asynchronous tasks are folded into a synchronous task that will be executed on the nextTick.
Additional analysis
MutationObserver
A brief introduction to MutationObserver: MO is an HTML5 API that monitors DOM changes. It can listen for child node deletion, attribute modification, text content modification on a DOM object, etc.
The call procedure binds it with a callback to get the MO instance, which is triggered when the MO instance listens for changes. The MO callbacks here are microtasks and are executed in microTask.
Const Observer = new MutationObserver(callback) const textNode = 'want to listen to Don' observer.observe(textNode, callback) {characterData: true // characterData: true})Copy the code
setImmediate(fn, 0)
Run the specified code as soon as the loop event task completes. This method is used to place long-running operations in a callback that is executed as soon as the browser completes the rest of the statement.
Application scenarios
-
Manipulate the DOM after the data changes
Reason: Vue performs DOM updates asynchronously. As soon as a data change is observed, Vue will open a queue and buffer all data changes that occur in the same event loop. If the same Watcher is triggered multiple times, it will only be pushed into the queue once.
-
The created() hook function manipulates the DOM
The DOM is not rendered when the created() hook function executes.
DOM manipulation by the Created () hook function in the Vue lifecycle must be placed in the vue.nexttick () callback. The DOM doesn’t actually render at the time the Created () hook function executes, and DOM manipulation is useless, so make sure you put the DOM manipulation js code in the view.nexttick () callback. The mounted() hook function is the mounted() hook function, because it executes when all DOM operations are completed.