Vue. Detailed explanation of nextTick implementation of js principle analysis

  • 2021-08-12 02:01:08
  • OfStack

Preface

tips: The first technical article is short, mainly in the form of words and key codes, hoping to help everyone. (If there are any inaccuracies, please correct them.)

The Function and Usage of nextTick

Usage: nextTick receives a callback function as a parameter, which is used to delay the callback until the next DOM update. If no callback function parameter is provided and in the environment supporting Promise, nextTick will return 1 Promise.
Applicable scenario: During the development process, developers need to do some operations on the new DOM after updating the data. In fact, we could not operate the new DOM at that time, because it had not been rendered again at this time, and nextTick came in handy at this time.

Implementation principle of nextTick

Let's introduce the working principle of nextTick:

First of all, we should understand that after updating the data (status), the DOM update action is not synchronous, but asynchronous. There is a queue in Vue. js. Whenever rendering is needed, Watcher will be pushed to this queue, and Watcher will trigger the rendering process in the next event cycle. Here we may have two questions:

**1. Why is updating DOM asynchronous? **

We know that since Vue 2.0, virtual DOM has been used for rendering, and change detection is only sent to the component level, while local rendering is carried out inside the component through diff (comparison) of virtual DOM. If the component receives two notifications in the same event cycle, will the component render twice? In fact, the 1-Event Loop component will render only once after all state modifications have been completed.

**2. What is an event loop? **

javascript is a single-threaded scripting language, It has non-blocking characteristics, It is non-blocking because when processing asynchronous code, The main thread suspends the task, When the asynchronous task is processed, the callback of the asynchronous task will be executed according to the rule set by 1. Asynchronous tasks are divided into macro tasks (macrotast) and micro tasks (microtast), which will be assigned to different queues. When all tasks in the execution stack are executed, whether there are events in the micro task queue will be checked first, and the callback corresponding to the micro task queue events will be executed first until it is empty. Then perform a callback of events in the macro task queue. Repeating this process indefinitely to form an infinite loop is called an event loop.

Common micro-tasks include: Promise, MutationObserver, Object. observer, process. nextTick, etc

Common macro tasks include: setTimeout, setInterval, setImmediate, MessageChannel, requestAnimation, UI interactive events, and so on

How to register micro-tasks?

nextTick adds callbacks to the asynchronous task queue to delay execution, Before executing the callback, calling nextTick repeatedly, Vue will not be added to the task queue repeatedly, but only add one task to the task queue. Using nextTick repeatedly will only add the callback to the callback list cache. When the task triggers, it will empty the callback list and execute all callbacks in turn. The specific code is as follows:


const callbacks = []
let pending = false

function flushCallbacks(){ // Execute a callback 
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0 // Empty the callback queue 
  for(let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let microTimerFunc
const p = Promise.resolve()
microTimerFunc = () => { // Register micro-task 
  p.then(flushCallbacks)
}

export function nextTick(cb,ctx){
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }
  })
  if(!pending){
    pending = true // Will pending Set to true Ensure that tasks are not added repeatedly in successive event loops 
    microTimerFunc()
  }
}

Because the priority of micro-tasks is too high, macro tasks may be needed in some scenarios, so Vue provides a method withMacroTask that can force the use of macro tasks. The specific implementation is as follows:


const callbacks = []
let pending = false

function flushCallbacks(){ // Execute a callback 
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0 // Empty the callback queue 
  for(let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let microTimerFunc
// Add code 
let macroTimerFunc = function(){
  ...
}

let useMacroTask = false
const p = Promise.resolve()
microTimerFunc = () => { // Register micro-task 
  p.then(flushCallbacks)
}

// Add code 
export function withMacroTask(fn){
  return fn._withTask || fn._withTask = function()=>{
    useMacroTask = true
    const res = fn.apply(null,arguments)
    useMacroTask = false
    return res
  }
}

export function nextTick(cb,ctx){
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }
  })
  if(!pending){
    pending = true // Will pending Set to true Ensure that tasks are not added repeatedly in successive event loops 
    // Modify code 
    if(useMacroTask){
      macroTimerFunc()
    }else{
      microTimerFunc()
    }
  }
}

The above provides an withMacroTask method to force the use of macro tasks, and controls whether to use registered macro tasks through useMacroTask variables. The implementation of withMacroTask is very simple. First, set useMacroTask variables to true, then execute callbacks, and then change back to false after callback execution.

How do macro tasks register?

setImmediate is preferred for the registration macro task, but there are compatibility problems and it can only be used in IE, so MessageChannel is used as an alternative. If none of the above is supported, setTimeout will be used in the end. The specific implementation is as follows:


if(typeof setImmediate !== 'undefined' && isNative(setImmediate)){
  macroTimerFunc = ()=>{
    setImmediate(flushCallbacks)
  }
} else if(
  typeof MessageChannel !== 'undefined' && 
  (isNative(MessageChannel) || MessageChannel.toString() === '[Object MessageChannelConstructor]')
){
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = ()=>{
    port.postMessage(1)
  }
} else {
  macroTimerFunc = ()=>{
    setTimout(flushCallbacks,0)
  }
}

The implementation method of microTimerFunc is through Promise. then, but not all browsers support Promise. When it is not supported, it is demoted to macro task


if(typeof Promise !== 'undefined' && isNative(Promise)){
  const p = Promise.resolve()
  microTimerFunc = ()=>{
    p.then(flushCallbacks)
  }
} else {
  microTimerFunc = macroTimerFunc
}

If no callback is provided and the environment supports Promise, nextTick will return 1 Promise, which is implemented as follows:


export function nextTick(cb, ctx) {
  let _resolve
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }else{
      _resolve(ctx)
    }
  })

  if(!pending){
    pending = true
    if(useMacroTask){
      macroTimerFunc()
    }else{
      microTimerFunc()
    }
  }

  if(typeof Promise !== 'undefined' && isNative(Promise)){
    return new Promise(resolve=>{
      _resolve = resolve
    })
  }
}

The above is the design of nextTick operation principle, and the complete code is as follows:


const callbacks = []
let pending = false

function flushCallbacks(){ // Execute a callback 
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0 // Empty the callback queue 
  for(let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let microTimerFunc
let macroTimerFunc 
let useMacroTask = false

// Register macro tasks 
if(typeof setImmediate !== 'undefined' && isNative(setImmediate)){
  macroTimerFunc = ()=>{
    setImmediate(flushCallbacks)
  }
} else if(
  typeof MessageChannel !== 'undefined' && 
  (isNative(MessageChannel) || MessageChannel.toString() === '[Object MessageChannelConstructor]')
){
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = ()=>{
    port.postMessage(1)
  }
} else {
  macroTimerFunc = ()=>{
    setTimout(flushCallbacks,0)
  }
}

// Micro-task registration 
if(typeof Promise !== 'undefined' && isNative(Promise)){
  const p = Promise.resolve()
  microTimerFunc = ()=>{
    p.then(flushCallbacks)
  }
} else {// Degradation processing 
  microTimerFunc = macroTimerFunc
}

export function withMacroTask(fn){
  return fn._withTask || fn._withTask = function()=>{
    useMacroTask = true
    const res = fn.apply(null,arguments)
    useMacroTask = false
    return res
  }
}

export function nextTick(cb,ctx){
  let _resolve
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }else{
      _resolve(ctx)
    }
  })
  if(!pending){
    pending = true // Will pending Set to true Ensure that tasks are not added repeatedly in successive event loops 
    // Modify code 
    if(useMacroTask){
      macroTimerFunc()
    }else{
      microTimerFunc()
    }
  }

  if(typeof Promise !== 'undefined' && isNative(Promise)){
    return new Promise(resolve=>{
      _resolve = resolve
    })
  }
}

The above is a complete introduction to the implementation principle of nextTick.

References

Vue. js is simple

Summarize


Related articles: