Vue.nextTick

What is nextTick

Vue's official document says, "perform a delayed callback after the next DOM update cycle. Use this method immediately after modifying the data to get the updated dom. " , why does such an API appear? The main reason is that Vue uses asynchronous execution to update dom. As long as it listens for data changes, Vue will open a queue and buffer all data changes in the same event cycle.

Use scenario

Although the official has always suggested that we use data-driven scheme to write code, sometimes it is inevitable to operate dom.

<div v-for='item of list' :key='item' class='list-item'>
  {{ item }}
</div>
list = [1,2,3,4]
When we push a number into the list in mounted, we immediately get the length of the class='list-item 'element, which is still 4, not 5.
Because the update of DOM in Vue is asynchronous, the DOM is printed immediately and not updated. At this time, the length can be obtained correctly through nextTick.

this.$nextTick(() => {
    console.log(document.getElementByClassName('list-item').length)
})
Copy code

What if the state changes many times in an event cycle?

If the same watcher is triggered multiple times, it will only be pushed into the queue once. This de duplication of data in buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event cycle "tick", Vue refreshes the queue and performs the actual (de duplicated) work. In general, it means de duplication. What's the internal details? Let me tell you later.

event loop

We all know that JS is a single thread non blocking language, which means that the main thread can only execute one task at a time. First, understand the basic concepts: Micro task, macro task, asynchronous task, synchronous task.

Micro task

Micro tasks, also called jobs, are common: 1. Process. Nexttick (node) 2. Promise 3. Musionobserver (New HTML5 feature) 4. Messe

Macro task

Macro task is also called task. Common tasks include: 1. Script (overall code) 2.setTimeout 3.setInterval 4.setImmediate 5.I/O 6.UI render 7.MessageChannel

Synchronize tasks

It refers to that tasks queued on the main thread can only be executed after the previous task is completed, for example, 1 + 2. Typical case

function loop() {
    while(true) {}
}
loop()

console.log(1)
Copy code

The execution loop will occupy the main thread and enter an infinite loop. If the previous loop is not completed, the next 1 will not be printed

Asynchronous task

It refers to a task that does not enter the main thread but enters the "task queue". Only when the main thread task is completed, the "task queue" starts to notify the main thread and requests to execute the task, the task will enter the main thread for execution,

function loop () {
    setTimeout(loop, 0);
}
loop()
Copy code

When executing loop. It seems to be an infinite loop state, but in fact, it will not cause the page to get stuck. You can still do other things, because setTimeout is an asynchronous task, and they will exit the main thread after executing once.

What is event loop

This figure gives a complete description of the browser's Event Loop. I will talk about the specific process of executing a JavaScript code. When executing a piece of code, first perform the synchronization task, then the micro task, and then the macro task; 1. Execute the global Script synchronization code, some of which are synchronous statements, some are asynchronous statements (such as setTimeout, etc.); 2. After the global Script code is executed, the call Stack will be emptied; 3. Take out the callback task at the head of the queue from the micro task queue, and put it into the call Stack for execution. After the execution, the length of the micro task queue will be reduced by 1; 4. Continue to take out the tasks at the head of the team and put them into the call Stack for execution, and so on, until all tasks in the microtask queue have been executed. Note that if another microtask is generated during the execution of microtask, it will be added to the end of the queue and will be called for execution in this cycle; 5. All tasks in the microtask queue have been executed, at this time, the microtask queue is an empty queue and the call Stack is also empty; 6. Take out the tasks in the macro queue at the head of the queue and put them into the Stack for execution; 7. After execution, the call Stack is empty; repeat step 3-7; (Event Loop)

Explore the implementation principle of nextTick

As mentioned earlier, Vue uses asynchronous execution when updating DOM. As long as it listens for data changes, Vue will open a queue and buffer all data changes in the same event cycle. So we just need to put the callback to be executed into the micro task or macro task to execute.

According to this idea, let's look at the implementation in Vue:

1. There is a callback queue. 2. Using nextTick multiple times is only one time, so you need to limit the use of pending. Otherwise, the page will see a continuous process of change, and the experience is very bad. 3. According to different platforms, browser environment, downward compatibility, the bottom solution is setTimeout. 4. The order is: promise > musionobserver > setimmediate > setTimeout

Source code

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

// Mark whether it is a micro task here, and special handling will be done to the event in other modules
export let isUsingMicroTask = false

// Drop the queue
const callbacks = []

// =============================
// Status flag, execute once only
let pending = false

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 used (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

// 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)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. 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 (typeof setImmediate !== '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
  callbacks.push(() => {
    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


  // Use as a Promise (new since 2.1.0)
  // New since 2.1.0: if no callback is provided and in the environment supporting Promise,
  // Then a Promise is returned. Please note that Vue doesn't bring Promise's polyfill, so if your target browser doesn't support Promise natively (ie: what do you want me to do), you have to provide your own polyfill.
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Copy code

Happy last weekend

Reference article

1.Take you through the Event Loop

Keywords: Vue iOS html5 Javascript

Added by dylan001 on Sat, 09 May 2020 07:55:40 +0300