Event Loop and nextTick of Vue

event loop

The js language is single-threaded. In order to coordinate events, scripts, user interaction, rendering, networking and other behaviors and prevent blocking of the main thread, js introduces the concept of event loop.

There are many types of Event Loop, which can be divided into window event loop s and Worker event loops by thread.Each JavaScript thread has a separate Event Loop, so different Workers have different Event Loops that run independently.

According to the running environment, event loop can be roughly divided into event loops in browser environment and event loops in node.js environment.They implement event loops differently and do not necessarily follow the WHATWG standard. In contrast, event loops in browser environments are more standards compliant.

Some basic concepts:

  1. Execution context: When the JS engine executes a global JS code, function or eval statement, it generates an execution context object with properties such as variable object (VO), scope, this, etc.
  2. Function Call Stack: When a function is called, the JS engine push es the execution context of the function into the function call stack and pop s out when it is finished.

Browser Environment

First look at the criteria: WHATWG HTML Standard:

An event loop has one or more task queues. A task queue is a set of tasks.

There is one or more task queues in an event loop.task queue is a collection of tasks.

A source: One of the task sources, used to group and serialize related tasks

The task source groups and serializes tasks.

Per its source field, each task is defined as coming from a specific task source. For each event loop, every task source must be associated with a specific task queue.

Each task comes from a special task source.For each event loop, each task source is associated with a special task queue.
The main task source s are:

  1. Tasks: script (whole code), setTimeout, setInterval, setImmediate(ie, node.js), I/O, MessageChannel;
  2. microtask: Promise, MutationObserve, process.nextTick(node.js)

These task source s can distribute task or microtask, such as Promise's three prototype methods then, catch, final's callback function is the real microtask.

Continue to look at the criteria:

Each event loop has a currently running task, which is either a task or null. Initially, this is null. It is used to handle reentrancy.

Each event loop has an executing task, but it can also be null.

Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm.

Each event loop has a microtask queue, note that there is one, not many.Of course, this is just a standard, not an implementation, there is more than one microtask queue in the node.js environment.
Read more directly from Standards: WHATWG HTML Standard.

The flow of the event loop:

In the browser environment:

  1. The first event loop executes the entire code of the script, pushing the global context into the call stack for execution.Synchronous tasks encountered during execution are directly pushed into call stack to execute. Asynchronous tasks encountered are processed by other threads in the browser background. When the asynchronous tasks meet the criteria, they are placed into microtask queue.
  2. When the task of this event loop is executed (that is, the call stack has only the global context), all the microtasks in the microtask queue are executed.Generally, these microtasks are executed in the order in which they are queued.Once it is a microtask's turn, its execution context is pushed into the call stack to execute, and when synchronous and asynchronous tasks are encountered during execution, the process is the same as the first step.When the stack frame in the call stack is empty, the microtask is executed and the next microtask in the microtask queue is executed.
  3. After performing all the microtasks, the browser will determine if UI rendering is required.Render if needed; go into the next event loop if not needed.
  4. In the second event loop, remove the first task from a task queue, press it into the call stack to execute,...
Promise.resolve(2).then(v => {
    console.log('Promise1')
    Promise.resolve(2).then(v => console.log('Promise1 Triggered Promise2'))
})
setTimeout(_ => console.log('setTimeout'))

The output is: Promise1 => Promise1 triggered Promise2 => setTimeout.
This is because Promise2 was triggered when Promise1 was executed in the first event loop, and Promise2 was added to the microtask queue, which is then executed in the event loop.

node.js environment

Reference link:

  1. Profiling event loops for nodejs
  2. Node.js event loop, timer and process.nextTick

Each event cycle in node.js consists of six phases:

   ┌───────────────────────────┐
┌─>│          `timers`         │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │    `pending callbacks`    │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     `idle`, `prepare`     │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │          `poll`           │<─────┤ `connections`,│
│  └─────────────┬─────────────┘      │  `data`, etc. │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │          `check`          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤     `close callbacks`     │
   └───────────────────────────┘

Each phase has a FIFO queue to store callbacks to be executed.
Generally speaking, when an event loop enters a certain stage, it will perform some specific actions of that stage and then perform callbacks in the queue for that stage until the queue is exhausted or the maximum number of callbacks that can be invoked in the current stage is reached.All process.nextTick callbacks are then executed, then all microtask s are executed, and the final event loop proceeds to the next stage.
Task sources for task and microtask in node.js:

  1. task: timer(setTimeout,setInterval), setImmediate, I/O...
  2. microtask: process.nextTick, promise...

Node.js implements libuv (a C-function library that implements Node.js event looping and all platform asynchronous behavior), and the node.js event looping mechanism is implemented inside it, Event Loop Core Code (C language):

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
    int timeout;
    int r;
    int ran_pending;
    r = uv__loop_alive(loop);    //Does the event loop survive
    if (!r)
        uv__update_time(loop);    //Update loop->time to current time
    //If the event loop survives and does not stop
    while (r != 0 && loop->stop_flag == 0) {
        uv__update_time(loop);
        // timers phase
        uv__run_timers(loop);
        // pending callbacks phase
        ran_pending = uv__run_pending(loop);
        // idle phase
        uv__run_idle(loop);
        // prepare phase
        uv__run_prepare(loop);

        timeout = 0;
        if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
            //Calculates the time difference timeout between the current time and the most recent timer timeout
            timeout = uv_backend_timeout(loop);
        // poll phase, which polls for I/O events, executes them, or blocks them until time exceeds timeout
        uv__io_poll(loop, timeout);
        // check phase
        uv__run_check(loop);
        // close callbacks phase
        uv__run_closing_handles(loop);

        if (mode == UV_RUN_ONCE) {
            uv__update_time(loop);
            uv__run_timers(loop);
        }
        
        r = uv__loop_alive(loop);
        //Exit the event loop if it is not currently waiting for any asynchronous I/O or timer
        if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
            break;
    }

    if (loop->stop_flag != 0)
        loop->stop_flag = 0;

    return r;
}

timers phase

This phase executes a callback function for a timer (setTimeout, setInterval) that has reached the wait time.The execution time of uv_run_timers is controlled by the poll phase.

void uv__run_timers(uv_loop_t* loop) {
    struct heap_node* heap_node;
    uv_timer_t* handle;

    for (;;) {
        //Remove the timer heap_node in timer_heap that has the closest timeout
        heap_node = heap_min((struct heap*) &loop->timer_heap);
        if (heap_node == NULL)
            break;
        //Get handle to heap_node
        handle = container_of(heap_node, uv_timer_t, heap_node);
        //Judges whether the timeout time of the handle of the most recent timer is greater than the current time, if greater than the current time, indicates that it has not yet timed out and jumps out of the cycle
        if (handle->timeout > loop->time)
            break;
        //Stop Timer 
        uv_timer_stop(handle);
        //Restart the timer if handle->repeat is true
        uv_timer_again(handle);
        //Callback function to execute timer
        handle->timer_cb(handle);
    }
}

Loop out the timer in &loop->timer_heap and execute its callback function until the current timer is NULL or has not timed out.From this, it can be seen that the node.js environment will execute all the qualified tasks in the corresponding task queue at one time at a certain stage of the event cycle.

As to why process.nextTick does not appear in the flowchart, the explanation given in the node.js document is:

You may have noticed that process.nextTick() was not displayed in the diagram, even though it's a part of the asynchronous API. This is because process.nextTick()
is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of
the event loop. Here, an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.

The main idea is that process.nextTick is not technically part of the event loop, but in each phase of the event loop, tasks in nextTickQueue will be executed when the current operation is complete.The reason for this depends on how node.js implements process.nextTick.

For later microtask s (typically promise callbacks), they are executed at the end of each phase.
The exact execution time of the Promise callback depends on how import's Proise is implemented. In node\deps\npm\node_modules\es6-promise's asap.js, there are several ways to call the callback:

//1,**node**
function useNextTick() {
    // node version 0.10.x displays a deprecation warning when nextTick is used recursively
    // see https://github.com/cujojs/when/issues/410 for details
    return () => process.nextTick(flush);
}
//2,vertx
function useVertxTimer() {
    if (typeof vertxNext !== 'undefined') {
        return function() {
            vertxNext(flush);
        };
    }

    return useSetTimeout();
}
//3. Browser
function useMutationObserver() {
    let iterations = 0;
    const observer = new BrowserMutationObserver(flush);
    const node = document.createTextNode('');
    observer.observe(node, { characterData: true });

    return () => {
        node.data = (iterations = ++iterations % 2);
    };
}

Most use microtask to implement Promise, followed by tasks such as setTimeout.
We focus on the node.js environment, and if the user is using the Promise object provided by es6-promise, the callback function bound to that object will eventually be called in the callback of process.nextTick, so the promise callback is also executed at the end of each phase of the event loop.

pending callbacks phase

This phase executes the I/O callback function in pending_queue (the last loop was not completed and was deferred to the I/O callback of this loop).

  1. Non-I/O: timer (setTimeout, setInterval), microtask, process.nextTick, setImmediate...
  2. I/O: network I/O, file I/O, some DNS operations...

After idle, the preparephase is only used internally in the system and is unknown.

poll phase

Node.js transfers non-blocking I/O operations to the system kernel, which notifies Node.js via events to add appropriate callback functions to poll queue and wait for them to execute.
In the uv_run function, when the uv_io_poll (loop, timeout) is called into the poll phase, a timeout parameter is passed in, which is the time when the current time is closest to the timer threshold and also the blocking time of the poll phase.
When the kernel listens for event notification node s, it exits the poll phase directly if the time reaches timeout.
Actions performed during the poll phase:

  1. If poll queue is not empty, loop through the callback functions in the callback queue until the queue is exhausted or the maximum number of calls has been reached.
  2. If poll queue is empty:

    1. If setImmediate task is already queued, the event loop will end the poll phase and enter the check phase.
    2. If setImmediate task is not queued, the event loop waits for the I/O callback to be added to the poll queue and executes immediately.

check phase

This phase executes the callback function for setImmediate.SetImmediate is actually a special timer that runs at a specific stage of an event loop, and its callback is executed after the poll phase is complete.

close callbacks phase

This phase executes a handle function to the socket close event.If a socket or handle is suddenly closed (for example, socket.destroy()), the close event will be emitted at this stage, otherwise it will be emitted through process.nextTick().

nextTick in Vue.js

The implementation of nextTick in different versions of Vue.js varies. After Vue 2.5+, there is a separate JS file to implement nextTick. Let's look directly at the latest stable version: v2.6.10.

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

export let isUsingMicroTask = false

const callbacks = []
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.
    // Techinically 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
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}
  1. Simply put, first, Vue stores the callback function to be executed with an array of callbacks, which is pushed into the callbacks array whenever Vue.nextTick or vm.$nextTick is used.
  2. Next, Vue declares a flushCallbacks function that takes out (empties) all callbacks in the callbacks array and executes them.
  3. Vue then tries to turn flushCallbacks into a microtask or task to execute.Whether it's a microtask or a task depends on the environment in which Vue is currently running:

The general judgment process is as follows:

  1. The current environment has a native Promise? Promise.resolve().then (flushCallbacks):
  2. Is it an ie environment? setImmediate(flushCallbacks):
  3. Have a native Mutation Observer?New Mutation Observer (flushCallbacks):
  4. setTimeout(flushCallbacks, 0);

Tags: Javascript Vue socket IE

Posted on Sun, 08 Sep 2019 19:20:00 -0700 by Mega