Event loop

Event loops in the browser

In the process of javascript code execution, not only the function call stack is used to process the execution order of functions, but also the task queue is used to process the execution of other codes. The entire execution process is called the event loop. Event loops are unique within a thread, but task queues can have more than one. Task queues are divided into macro-tasks and micro-tasks.

Macro-task includes:

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

Micro-task includes:

  • process.nextTick
  • Promise
  • MutationObserver

The relationship between event loops, macro tasks, and micro tasks

The general conclusion is that the macro task is executed, and then the micro-task generated by the macro task is executed. If a new micro-task is generated during the execution of the micro-task, the micro-task is continued to be executed. After the completion of the micro-task, the macro-task is returned to the next round.

async function async1(){
    await async2()
    console.log('async1 end')}async function async2(){
   console.log('async2 end')
 }
 
 async1()
 
 setTimeout(function(){
   console.log('setTimeout')},0)
 
 new Promise(resolve= >{
    console.log('Promise')
    resolve()
 })
 .then(function(){
   console.log('promise1')
 })
 .then(function(){
   console.log('promise2')})Copy the code
Macro task Micro tasks
setTimeout async1 end
promise1
promise2

Combined with the flowchart, the output is as follows: AsynC2 end => Promise => ASYNC1 end => promise1 => promise2 => setTimeout

The $nextTick Vue

NextTick API description:

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

<template>
  <div id="app">
    <div  ref='foo'>
        {{foo}}
    </div>   
    <button @click="changeFoo">Change the foo</button>
  </div>
</template>

<script>
export default {
  data(){
      return{
          foo:'ready'}},methods: {changeFoo(){
       this.foo='1'
       console.log('foo '.this.foo,this.$refs['foo'].innerHTML) // The typed Html is still ready, but foo has changed to 1
        this.$nextTick(() = >{
         console.log('foo in NextTick'.this.foo,this.$refs['foo'].innerHTML)// You can print the innerHTML as 1})}}}</script>

<style>

</style>
Copy the code

In case you haven’t noticed, Vue performs DOM updates asynchronously. Whenever a data change is observed, 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 is important in buffering to remove duplicate data 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 tries to use native promise.then and MessageChannel for asynchronous queues, or 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.

NextTick source analysis

watcher

src\core\observer\watcher.js

// To mark watcher
let uid = 0** ** @param {*} Options Configuration items of watcher */export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {    
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
  

    this.id = ++uid // uid for batching


    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } 

Copy the code

watcher.update

src\core\observer\watcher.js

/** * DeP notifes Watcher to perform update */ dep. Notify (
update () {
    /* istanbul ignore else */
    // computed
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      / / team
      queueWatcher(this)}}Copy the code

Asynchronous update queue queueWatcher(watcher)

core\observer\scheduler.js

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  / / to heavy
  if (has[id] == null) {
    has[id] = true
    if(! flushing) {/ / team
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1.0, watcher)
    }
    // queue the flush
    if(! waiting) { waiting =true
      // Add the flushSchedulerQueue asynchronously
      nextTick(flushSchedulerQueue)
    }
  }
}


function flushSchedulerQueue () {
  / /...
  let watcher, id
  for (index = 0; index < queue.length; index++) {
    // Take out one watcher at a time
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    // The real operation is done by the run method
    watcher.run()
  }
  / /...

}
Copy the code

nextTick(flushSchedulerQueue)

src\core\util\next-tick.js

NextTick performs queue operations based on a specific asynchronous policy


/** * asynchronously update queue */
 
const callbacks = []
let pending = false

// Refresh the callback array
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // Traverse and execute
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
// timerFunc is defined according to the environment, and microtasks are preferred
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 &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  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 (typeofsetImmediate ! = ='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)}}// Put the cb function at the end of the callback queue
export function nextTick (cb? :Function, ctx? :Object) {
  let _resolve
  // Add a layer to handle exceptions
  callbacks.push(() = > {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')}}else if (_resolve) {
      _resolve(ctx)
    }
  })
  if(! pending) { pending =true
    // Execute the function asynchronously
    timerFunc()
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(resolve= > {
      _resolve = resolve
    })
  }
}

Copy the code

summary

  • Asynchronous: As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop.
  • Batch: If the same watcher is triggered more than once, it will only be pushed to the queue once. De-duplication is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop, “TICK,” Vue refreshes the queue to do the actual work.
  • Asynchronous strategy: Vue internally attempts to use native Promise.then, MutationObserver, or setImmediate for asynchronous queues, and setTimeout instead if none of the execution environments support it.