Event Loop is the core idea of JavaScript asynchronous programming, and it is also a hurdle that must be overcome in the advance of front-end. At the same time, it was a must-test for interviews, especially after Promise came along. This article starts with examples in real life, so that you can thoroughly understand the principle and mechanism of Event Loop and solve such interview questions with ease.

The pen test on that shitty street on Universe Street

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
});
console.log('script end');
Copy the code

Why is JavaScript single threaded?

We all know that JavaScript is a single-threaded language, which means you can only do one thing at a time. This is because JavaScript is designed as a browser scripting language to handle user interaction, networking, and DOM manipulation. This means that it has to be single-threaded, which can cause complex synchronization problems.

If JavaScript has two threads, one thread adds content to a DOM node, and the other thread removes it, which thread should the browser use?

Since Javascript is single-threaded, it’s like a one-window bank, where customers have to queue up one by one. Similarly, JavaScript tasks need to be executed one after another. If a task (such as loading a high-resolution image) is a time-consuming task, shouldn’t the browser be stuck? To prevent the main thread from blocking, JavaScript has the concept of synchronous and asynchronous.

Synchronous and asynchronous

synchronous

A function is synchronous if the caller gets the expected result when it returns. That is, once a synchronous method call starts, the caller must wait until the function call returns before continuing with the subsequent behavior. The following code will first bring up an alert box, and if you don’t click OK, all page interactions will be locked, and subsequent console statements will not be printed.

alert('Yancey');
console.log('is');
console.log('the');
console.log('best');
Copy the code

asynchronous

A function is asynchronous if the caller does not get the expected result when it returns, but needs to get it in the future by some means. Let’s say we send a network request, and we tell the main program to wait until it receives the data and let me know, and then we can do something else. We will be notified when the asynchrony is complete, but the program may be doing something else at this point, so even if the asynchrony is complete, we will have to wait until the program is idle to see what asynchrony has completed before executing it.

This is why timers do not accurately output the result of a callback function after the specified time.

setTimeout((a)= > {
  console.log('yancey');
}, 1000);

for (let i = 0; i < 100000000; i += 1) {
  // todo
}
Copy the code

Execution stack and task queue

Let’s review the data structure

  • Stack: A stack is an ordered collection of last-in, first-out (LIFO) elements that are newly added or to be removed are stored at one end, called the top of the stack, and at the other end, called the bottom of the stack. In the stack, the new elements are near the top of the stack, and the old elements are near the bottom of the stack. The stack stores Pointers to basic data types and objects, method calls, and so on in the compiler and memory of the programming language.

  • Queue: A queue is an ordered collection of first-in, first-out (FIFO) elements. New elements are added at the end of the queue and removed at the top. The latest element must be placed at the end of the queue. In computer science, the most common example is a print queue.

  • Heap: A heap is a special data structure based on a tree abstract data type.

As shown in the figure above, memory in JavaScript is divided into heap memory and stack memory,

The size of reference type values in JavaScript is not fixed, so they are stored in heap memory, where the system allocates storage automatically. JavaScript does not allow direct access to locations in heap memory, so we cannot directly manipulate the heap memory space of an object, but rather the reference to the object.

The underlying data types in JavaScript have a fixed size, so they are stored in stack memory. We can directly manipulate values stored in stack memory space, so the underlying data types are accessed by value. In addition, stack memory stores references to objects (Pointers) and space to run when functions are executed.

Here’s a comparison of the two storage methods.

Stack memory Heap memory
Store the underlying data types Store reference data types
According to the value of access Access by reference
Values stored are fixed in size Stored values vary in size and can be adjusted dynamically
The system automatically allocates memory space It is assigned by the programmer through code
Mainly used to execute programs Mainly used to store objects
Small space, high operating efficiency Large space, but relatively low operating efficiency
First in, last out, last in, first out Unordered storage, which can be obtained by reference

Execution stack

When we call a method, JavaScript generates an execution environment, also called an execution context, corresponding to that method. The execution environment holds the method’s private scope, the upper scope (the scope chain), the method’s parameters, as well as the variables defined in the scope and the reference to this when a sequence of methods is called. Since JavaScript is single-threaded, these methods are sequentially arranged in a single place, called the execution stack.

Task queue

An event queue is a queue that stores asynchronous tasks, in which tasks are executed in strict chronological order, with the tasks at the top of the queue being executed first and the tasks at the bottom of the queue being executed last. The event queue executes only one task at a time, and when that task is complete, the next task is executed. The execution stack is a run container similar to the function call stack. When the execution stack is empty, the JS engine checks the event queue. If the event queue is not empty, the event queue pushes the first task into the execution stack to run.

Event loop

We note that it is still possible to wait after the asynchronous code has finished because the program is doing other things and has time to see what asynchrony has done when it is idle. So JavaScript has a mechanism for handling synchronous and asynchronous operations called Event loops.

The following is a schematic of the event cycle.

In words, it would look something like this:

  • All synchronization tasks are executed on the main thread, forming an Execution Context Stack.

  • The asynchronous Task is placed in the Task Table, which is the asynchronous processing module in the figure above. When the asynchronous Task has a result, the function is moved to the Task queue.

  • Once all synchronization tasks in the execution stack have been executed, the engine reads the task queue and pushes the first task in the task queue into the execution stack.

The main thread repeats the third step, that is, whenever the main thread is empty, it will read the task queue. This process is repeated over and over again. This is called an event loop.

Macro and micro tasks

Microtasks, macro tasks and Event-loop This article uses interesting examples to explain macro tasks and microtasks. Here is a copy.

Take going to the bank for business as an example. When the teller at window 5 finishes dealing with the current customer, he starts to call the number to receive the next customer. We compare each customer to a macro task, and the process of receiving the next customer is to let the next macro task enter the execution stack.

So all the customers of the window are put into a task queue. Instead of registering an asynchronous Task to be placed in the Task queue (it will be placed in the Task Table), the Task queue is filled with asynchronous operations that have already been completed. Just like queuing in a bank, if you are not there when you are called, your current number will be invalid, and the teller will choose to skip the next customer’s transaction. When you return, you will have to take the number again.

When performing macro tasks, it is possible to intersperse some micro tasks. For example, after you finish your business, you ask the teller by the way: “Recently P2P explosion is very serious ah, is there any other safe investment way”. The teller said darkly, “Another fool has taken the bait,” and jabbered on a lot.

We analyzed the process. Although the old man had finished his normal business, he asked for financial information. At this time, the teller certainly could not say: “Please get a number from the back and get in line again”. So anything the teller can handle will be done before responding to the next macro task, which we can think of as a micro task.

The elder uncle listened, raised a 45 degree smile, said: “I just ask.”

Teller OS: “Fuck…”

This example illustrates: you are always your uncle in the current micro task is not complete, the next macro task!

To sum up, asynchronous tasks are divided into macrotasks and microtasks. Macro tasks go to one queue, while microtasks go to a different queue, and microtasks are superior to macro task execution.

Common macro and micro tasks

Macro tasks: Script (overall code), setTimeout, setInterval, I/O, Events, postMessage, MessageChannel, setImmediate (Node.js)

Microtasks: Promise.then, MutaionObserver, process.nexttick (node.js)

Let’s do a couple of problems

See if you can work out the following problem.

setTimeout((a)= > {
  console.log('A');
}, 0);
var obj = {
  func: function() {
    setTimeout(function() {
      console.log('B');
    }, 0);
    return new Promise(function(resolve) {
      console.log('C'); resolve(); }); }}; obj.func().then(function() {
  console.log('D');
});
console.log('E');
Copy the code
  • The first setTimeout is placed in the macro task queue, where the macro task queue is [‘A’]

  • Then execute the func method of obj and place setTimeout on the macro task queue [‘A’, ‘B’].

  • The function returns a Promise, and since this is a synchronous operation, prints ‘C’ first.

  • Then is placed on the microtask queue, which is [‘D’]

  • Log (‘E’); , print ‘E’

  • Since microtasks take precedence, print ‘D’ first

  • And then print ‘A’ and ‘B’

Look at a ruan Yifeng teacher out of the topic, in fact, it is not difficult.

let p = new Promise(resolve= > {
  resolve(1);
  Promise.resolve().then((a)= > console.log(2));
  console.log(4);
}).then(t= > console.log(t));
console.log(3);
Copy the code
  • First place the then() method of promise.resolve () on the microtask queue, which is [‘2’]

  • Then print synchronization task 4

  • Then put p’s then() method on the microtask queue, which is [‘2’, ‘1’]

  • Print synchronization task 3

  • Finally, print microtasks 2 and 1 in turn

When the Event Loop encounters async/await

We know that async/await is just syntactic sugar for generators, so don’t be afraid to just convert it to a Promise form. The following code is the classic form of the async/await function.

async function foo() {
  // await the preceding code
  await bar();
  // the code behind await
}

async function bar() {
  // do something...
}

foo();
Copy the code

Where the code before await is synchronous and will be executed directly when the function is called; And await the bar (); This can be converted to promise.resolve (bar()); The code after await is put in the Promise’s then() method. So the above code can be converted to the following form, which makes it clear, right?

function foo() {
  // await the preceding code
  Promise.resolve(bar()).then((a)= > {
    // the code behind await
  });
}

function bar() {
  // do something...
}

foo();
Copy the code

Going back to the theme of the universe street, isn’t it easy for us to “refactor” the code and then parse it?

function async1() {
  console.log('async1 start'); / / 2

  Promise.resolve(async2()).then((a)= > {
    console.log('async1 end'); / / 6
  });
}

function async2() {
  console.log('async2'); / / 3
}

console.log('script start'); / / 1

setTimeout(function() {
  console.log('settimeout'); / / 8
}, 0);

async1();

new Promise(function(resolve) {
  console.log('promise1'); / / 4
  resolve();
}).then(function() {
  console.log('promise2'); / / 7
});
console.log('script end'); / / 5
Copy the code
  • First print script start

  • Then add setTimeout to the macro task queue, which is [‘ setTimeout ‘]

  • Resolve (async2()) is a synchronous task, so print async2. Then add async1 end to the microtask queue. The microtask queue is [‘async1 end’]

  • Print promise1 and add promise2 to the microtask queue as [‘async1 end’, promise2]

  • Print the script end

  • Since microtasks take precedence over macro tasks, print async1 end and promise2 in sequence

  • Finally, print out the macro task setTimeout

About the controversy of this topic: the article has been published for about two days, and it has received many comments from my friends. Most of it is the order of async1 end and promisE2. I tested async1 end before Promise2 in Chrome 73.0.3683.103 for MAC and Node.js V8.15.1 In FireFox 66.0.3 for MAC test is async1 end after promise2.

Differences between Node.js and event loops in browser environments

With the upgrade to 11.x, the Event Loop mechanism changes. SetTimeout,setInterval, and setImmediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate Mediate Mediate Mediate Mediate

For details on the difference between Node.js and browser Event loops prior to 11.x, please refer to @Lani’s “What’s the difference between Browser and Node Event Loops?” So, no more nonsense here.

Introduction to Web Workers

It’s important to note that workers are a feature of the browser (that is, the hosting environment) and actually have very little to do with the JavaScript language itself. That said, JavaScript does not currently have any support for multithreaded execution.

So JavaScript is a single-threaded language! JavaScript is a single-threaded language! JavaScript is a single-threaded language!

Browsers can provide multiple instances of JavaScript engines, each running on its own thread, so you can run different programs on each thread. Each such independent multithreaded part of the program is called a Worker. This type of parallelization is called task parallelism because the emphasis is on dividing programs into blocks to run concurrently. Below is the flow diagram of Worker’s operation.

A Web Worker instance

The following uses a factorial example to talk about the usage of Worker.

Start by creating a new index.html file and go directly to the code:

<body>
  <fieldset>
    <legend>Calculating factorial</legend>
    <input id="input" type="number" placeholder="Please enter a positive integer" />
    <button id="btn">To calculate</button>
    <p>Calculation results:<span id="result"></span></p>
  </fieldset>
  <legend></legend>

  <script>
    const input = document.getElementById('input');
    const btn = document.getElementById('btn');
    const result = document.getElementById('result');

    btn.addEventListener('click', () = > {const worker = new Worker('./worker.js');

      // Send a message to the Worker
      worker.postMessage(input.value);

      // Receive messages from the Worker
      worker.addEventListener('message', e => {
        result.innerHTML = e.data;

        // Close Worker after using it
        worker.terminate();
      });
    });
  </script>
</body>
Copy the code

Create a new work.js file in the same directory with the following contents:

function memorize(f) {
  const cache = {};
  return function() {
    const key = Array.prototype.join.call(arguments.', ');
    if (key in cache) {
      return cache[key];
    } else {
      return (cache[key] = f.apply(this.arguments)); }}; }const factorial = memorize(n= > {
  return n <= 1 ? 1 : n * factorial(n - 1);
});

// Listen for messages from the main thread
self.addEventListener(
  'message'.function(e) {
    // Respond to the main thread
    self.postMessage(factorial(e.data));
  },
  false,);Copy the code

We’re going to end with two questions

Eventloop isn’t scary. What’s scary is meeting promises. It won’t hit me, will it? Yeah.

The first question

const p1 = new Promise((resolve, reject) = > {
  console.log('promise1');
  resolve();
})
  .then((a)= > {
    console.log('then11');
    new Promise((resolve, reject) = > {
      console.log('promise2');
      resolve();
    })
      .then((a)= > {
        console.log('then21');
      })
      .then((a)= > {
        console.log('then23');
      });
  })
  .then((a)= > {
    console.log('then12');
  });

const p2 = new Promise((resolve, reject) = > {
  console.log('promise3');
  resolve();
}).then((a)= > {
  console.log('then31');
});
Copy the code
  • First print promise1

  • Then add THEN11, ‘promise2’ to the microtask queue [‘then11’, ‘promise2’]

  • Print out promise3 and add THEN31 to the microtask queue [‘ THEN11 ‘, ‘promise2’, ‘then31’]

  • Then print then11, promise2, then31, and the microtask queue is empty

  • Add THEN21 and THEN12 to the microtask queue, which is [‘ THEN21 ‘, ‘then12’]

  • Then21 is printed, then12 is printed, and the microtask queue is empty

  • Add THEN23 to the microtask queue, which is [‘then23’]

  • Print out the then23

The second problem is

When a Promise is returned in the then() method, the second completion handler of P1 hangs under the then() method of the returned Promise, so the output order is as follows.

const p1 = new Promise((resolve, reject) = > {
  console.log('promise1'); / / 1
  resolve();
})
  .then((a)= > {
    console.log('then11'); / / 2
    return new Promise((resolve, reject) = > {
      console.log('promise2'); / / 3
      resolve();
    })
      .then((a)= > {
        console.log('then21'); / / 4
      })
      .then((a)= > {
        console.log('then23'); / / 5
      });
  })
  .then((a)= > {
    console.log('then12'); / / 6
  });
Copy the code

The last

Welcome to follow my wechat public number: the front of the attack

reference

JavaScript You Don’t Know (Middle Volume) by Kyle Simpson

This time, thoroughly understand the JavaScript execution mechanism

Let’s talk about JavaScript event loops in a nutshell

Microtasks, macro tasks, and Event-loops

Front end Basics: Detailed diagrams of JavaScript memory space

Explain the Event Loop mechanism in JavaScript

Eventloop is not scary. It’s scary to meet a Promise

Figure out the JavaScript engine Event Loop

JavaScript threading versus event mechanisms