Understanding the asynchronous

Asynchrony and synchronization

Most engineers are probably more familiar with synchronization than asynchrony.

To compare synchronous and asynchronous, you can divide the process of calling a function into two parts: performing an operation and returning a result.

When a program calls a function synchronously, it executes the operation immediately and waits for the result to return before continuing, that is, synchronous execution blocks.

Asynchronism separates the action from the result in time, executing the action now and returning the result at some point in the future. While waiting for the result to return, the program will continue to execute the following code. That is, asynchronous execution is non-blocking.

Here’s another simple example of synchronous versus asynchronous.

The following code defines the synchronous function syncAdd and the asynchronous function asyncAdd. Calling syncAdd(1,2) will wait for the result before executing the following code. A call to asyncAdd(1,2,console.log) will continue until the result is returned and printed 1 second later.

function syncAdd(a, b) {
  return a + b; 
}
syncAdd(1.2) 
function asyncAdd(a, b, cb) { 
  setTimeout(function() {
    cb(a + b); 
  }, 1000) 
}
asyncAdd(1.2.console.log)
Copy the code

Asynchrony and callback

If you call JavaScript’s asynchronous functions a lot, you might conclude that asynchronous operations take the form of callback functions.

After all, everything from browser-side DOM events, AJAX requests, timers, to Node.js file reads and writes, and multiple processes are all in the form of callbacks. But is that really the case?

Here is a simple code that defines a JSON object, a, prints it to the console, and increses the couter.index property of object A by 1.

var a = { 
  counter: { 
  	index: 1}}console.log( a ); 
a.counter.index++;
Copy the code

If you copy and paste the code above into the browser console, you should see that the console prints a JSON object that might not be what you expected :{conter:{index: 2}}.

The reason for this is that the browser runs the code with a delay in executing the I/O operations involved in console printing. You might wonder if the console is printing something like a “shallow copy” of object A, but it’s easy to deny that, and you’ll see that the value of the printed object has not changed once you perform the increment operation again.

Since not all asynchrony callbacks are called, are all callbacks executed asynchronously? The answer is also no.

Such as the array prototype forEach function, or changing the call to which this refers.

Principle of asynchronous

Having reviewed the basic concepts of asynchrony, here’s a closer look at how asynchrony works.

Event loop

For most languages, asynchrony is achieved by starting additional processes, threads, or coroutines. JavaScript is the advantage of single threads how do you do asynchrony?

Why can a single thread be asynchronous?

There is no special dark magic, just handing off some operations to other threads and using a mechanism called “event loop” (also known as “event polling”) to process the returned results.

Here’s some simplified code to help you quickly understand the event loop mechanism.

The array eventLoop represents the event queue (also known as the “task queue”) that holds the task events that need to be executed. The object event variable represents the task events that need to be executed.

The event loop is represented by a never-ending while loop, called a tick for each loop.

For each tick, if there are waiting events in the queue, an event is fetched from the queue and executed, usually in the form of a callback function.

var eventLoop = [];
var event;
while (true) {
  if (eventLoop.length > 0) {
    event = eventLoop.shift();
    try {
      event();
    } catch(err) { reportError(err); }}}Copy the code

So where does this event queue come from?

Take an AJAX request for example. When we make an AJAX request, the browser assigns the request to the network thread to handle it. When the corresponding network thread receives the returned data, it inserts the callback function into the event queue.

The same goes for setTimeout and setInterval. When we do setTimeout, we don’t put the callback directly into the event queue. All it does is hand it off to the timer thread, and when the timer arrives, it puts the callback in the event queue so that it can be fetched and executed in a future tick round.

The implicit problem with this is that if there are already other events in the event queue, the callback will queue up.

Therefore, the accuracy of setTimeout/setInterval timers is not high. To be precise, it only ensures that the callback function does not run before the specified interval, but may run at that point or after, depending on the state of the event queue.

The event queue

SetTimeout /setInterval exposes a defect of the event queue: the event queue is executed in first-in, first-out order. Therefore, if the queue is long, the last event, even if it is “urgent”, must wait for the previous task to complete first.

JavaScript solves this problem by setting up multiple queues and executing them according to priority.

The following code verifies that JavaScript has two queues with different priorities, shown in green and red, where the red queue has a higher priority than the green queue.

This code defines four asynchronous functions f1, F2, f3, f4, where:

  • Function f1 inserts a console print task into the green queue via timer setTimeout, printing the number 1;

  • Function f2 inserts a console print task into the red queue via Promise, printing the number 2;

  • Function f3 inserts a callback function into the green queue via timer setTimeout, which calls the console to print the number 3 and calls function f2;

  • The function F4 inserts a callback function into the red queue via Promise, which calls the console to print the number 4, and calls the function F1.

function f1() {
  setTimeout(console.log.bind(null.1), 0)}function f2() {
  Promise.resolve().then(console.log.bind(null.2))}function f3() {
  setTimeout(() = > {console.log(3)
  	f2()
  },
  0)}function f4() {
  Promise.resolve().then(() = >{
    console.log(4) 
  	f1()
  }
}
f3() f4()
Copy the code

The result of this code can be seen in the following figure. When f3 and f4 are called, both the green queue and the red queue are called

Insert an anonymous callback function.

  • For the first tick, since the red queue has a higher priority, the red anonymous function is executed first, the console prints the number 4, and then the function F1 is called to insert a print function into the green queue.
  • On the second tick, according to the first-in-first-out principle, the anonymous function is called to print the number 3, and the function F2 is called to insert a print function into the red queue.
  • The third tick calls the print function in the red queue, and the console prints the number 2. The fourth tick calls the print function in the green queue, and the console prints the number 1.

\

The red Queue and green Queue are generally called Macro Task Queue and Micro Task Queue, as well as Task Queue and Job Queue.

No matter how, we need to remember is that different queue priority, each event loop will obtain from the priority queue, only when the priority queue is empty will get events from the low priority queue, queue at the same level between the event there is no priority, only follow the principle of first in first out.

A common asynchronous function has the following priority, descending from top to bottom:

Process.nexttick (node.js)> MutationObserver(browser)/promise.then(catch, finnally)> setImmediate(IE) > setTimeout/setIntervalrequestAnimationFrame > other I/O operations/browser DOM events

Deal with asynchronous

Since the callback function form is very unreadable, try to convert the callback form into a return when writing code

The form of the Promise object, on the one hand, because ES6 standard provides the original Promise object and method, on the other hand, Promise

It can be used with the async/await keyword, for example, or converted into an Observable. So the more to

More and more third-party library asynchronous functions are starting to return Promise objects.

Let’s use asynchronous functions in the form of Promises as an example to explain some common asynchronous scenarios.

Asynchronous serial port

Converting asynchrony to serial execution (synchronous operation) is a very common operation, so let’s review the basic implementation with a simple example.

Consider asynchronous functions asyncF1 and asyncF2, and asyncF2 executes depending on the return of asyncF1. Use what Promise provides

The then function can be implemented directly with the following pseudocode:

asyncF1() .then(data = >asyncF2(data)) .then(() = >{... }) .catch(e = >console.error(e))
Copy the code

If async/await is used to get rid of chained calls, the code is more readable:

(async function() {
  try {
    const data = await asyncFn1() 
    const result = await asyncFn2(data)
    ...
  } catch(e) {
    console.error(e)
  }
})()
Copy the code

Review the basics to make it more difficult. Suppose we now call asyncF n times in order.

Instead of simply using a for loop or array forEach, this can be done using the array reduce function.

For example:

[1...n].reduce(async(lastPromise, i) = >{
  try {
    await lastPromise console.log(await asyncF())
  } catch(e) {
    console.error(e)
  }
},
Promise.resolve())
Copy the code

Step it up again and use a written test to deepen your understanding.

We now need to print the array [1,2,3,4,5] with an initial 1000ms delay and a growth delay of 500ms per print. Print the result

As follows:

0s: 1
1s: 2
2.5s: 3
4s: 4
7s: 5
Copy the code

This problem is also a typical example of changing multiple asynchronous functions to be executed sequentially, so it can also be done with the reduce function. Due to the introduction of

Incremental delay execution, so all need to get the last execution of the delay time. The specific code is as follows:

const arr = [1.2.3.4.5] arr.reduce(async(prs, cur, index) = >{
  const t = await prs const time = index === 0 ? 0 : 1000 + (index - 1) * 500
  return new Promise((res) = >{
    setTimeout(() = > {console.log(cur);
      res(time)
    },
    time)
  })
},
Promise.resolve(0))
Copy the code

Asynchronous parallel

Parallelism is also a common asynchronous scenario. Focus on the following three static functions.

(1) promise.all ([promise1…… promiseN])

Calling promise.all returns a new Promise instance that will call resolve when all promises in the argument are resolved; If the promise parameter has a failed promise, then this instance returns the result of the first failed promise.

It can be used when executing asynchronous functions with strong consistency, such as sending multiple requests to update a large form data

Update different data, and abandon the commit if one request fails to update.

(2) the Promise. AllSettled (promiseN] [promise1…)

Calling the function promise. allsellTED returns a new Promise instance that will be held for all given promises

Row completion returns an array of objects, each representing the corresponding Promise result.

This function is used when multiple asynchronous functions need to be executed concurrently, and the results of these asynchronous functions are independent of each other. Like sending more than one at a time

AJAX requests to update multiple pieces of data separately.

(3) Promise. Race (promiseN] [promise1…)

Calling the function promise. race returns a new Promise instance, once one of the promises in the argument completes execution

The promise instance returns the execution result of the corresponding promise.

This function creates a “race” between multiple concurrent functions to pick the one that completes first. Like trying to load images from multiple sites

The source.

Exception handling

Promises have a hidden “pit” where an internal exception cannot be caught externally by a try/catch when an internal exception occurs

“, the system automatically enters the Rejected state. So the following code is equivalent:

new Promise((resolve, reject) = >{
  throw new Error(0)})Copy the code

It is recommended to use the catch clause rather than catch the Promise exception in the THEN clause, as this can catch the THEN clause

Exception information in.

Promise.resolve(1).then(data = >{
  const arr = data.split(' ')
},
error = >{}) Promise.resolve(1).then(data = >{
  const arr = data.split(' ')}).catch(error = >{})
Copy the code

Limitations of Promises

While promises have obvious advantages over callbacks, they still have some limitations, with at least two areas of concern.

Executed immediately

When a Promise instance is created, the internal code is executed immediately and cannot be stopped externally. For example, you can’t cancel the super

Asynchronous calls that consume time or performance can easily waste resources.

A single execution

Promises deal with “one-shot” problems, and because a Promise instance can only resolve or reject once, it can’t handle certain scenarios that require a continuous response. For example, when uploading files to obtain progress, the default is to use the way of event monitoring to achieve.

So promises are not a panacea, and a thorough understanding of their strengths and weaknesses can help us make better use of promises. In fact, there are solutions to these problems, such as using RxJS, I expect you to think more and accumulate more in your work to find more and better solutions.


— Public account “Grace front” —

For every aspiring “front-end ER” to lead the way, build dream aspirations.

Just to be an excellent front end Engineer!