Amazing! Visual js: dynamic graph demonstrates the process of Promises & Async / Await!

Posted May 25, 202012 min read

Reason

Have you run js code that does not run as you expected?

For example:a function was executed at a random, unpredictable time, or was delayed.

At this time, you need to introduce a very cool new feature from ES6:Promise to deal with your problem.

In order to understand Promises in depth, I made some animations on a sleepless night to demonstrate the operation of Promises. My curiosity for many years was finally realized.

For Promise, why do you use it, how does it work at the bottom, and how do we write it in the most modern way?

Introduction

When writing JavaScript, we often have to deal with tasks that depend on other tasks!

For example:we want to get a picture, compress it, apply a filter, and then save it.

First, use the getImage function to get the picture we want to edit.

Once the image is successfully loaded, pass the image value to an ocmpressImage function.

After the image has been successfully resized, apply a filter to the image in the applyFilter function.

After the picture is compressed and added filters, save the picture and print the successful log!

Finally, the code is very simple as shown in the figure:

Did you notice? Although the above code can also get the results we want, but the completion process is not friendly.

A large number of nested callback functions are used, which makes our code particularly difficult to read.

Because many nested callback functions are written, these callback functions depend on the previous callback function, which is often called callback hell .

Fortunately, the Promise in ES6 can handle this situation well!

Let's see what a promise is and how it can help us in a situation similar to the above.

Promise syntax

ES6 introduced Promises. In many tutorials, you may read something like this:

Promise is a placeholder for a value that will either resolve or reject at some time in the future.

For me, such an explanation has never made things clearer.

In fact, it just makes me feel that Promise is a strange, vague, unpredictable piece of magic.

Next let us see what a promise is really?

We can create a promise using a Promise constructor that receives a callback function.

Cool, let's try it!

Wait, what is the return value you just got?

Promise is an object that contains a status PromiseStatus and a value PromiseValue.

In the above example, you can see that the value of PromiseStatus is pending, and the value of PromiseValue is undefined.

But-you will never interact with this object, you cannot even access the two properties PromiseStatus and PromiseValue!

However, when using Promises, the values of these two properties are very important.


The value of PromiseStatus, which is the status of Promise, can be one of the following three values:

  • fulfilled: promise has been resolved. Everything is fine, no errors occur inside promise.
  • rejected: promise has been rejected. Oops, something went wrong.
  • pending: promise has not yet been resolved or rejected, it is still in pending state

Ok, all this sounds great, but when does the status of promise be pending, fulfilled or rejected? Why is this status important?

In the above example, we just passed a simple callback function () => {} to the Promise constructor.

However, this callback function actually accepts two parameters.

  • The value of the first parameter is often called resolve or res, it is a function that will be called when Promise should resolve resolve.
  • The value of the second parameter is often called reject or rej, and it is also a function that should be called when Promise has some errors and should be rejected.

Let's try to see the logs we get when we call the resolve or reject method.

In my example, the resolve method is called res, and the reject method is called rej.

Great! We finally know how to get rid of pending state and undefined value!

  • When we call the resolve method, the state of promise is fulfilled.
  • When we call the reject method, the status of promise is rejected.

Interestingly, I asked(Jake Archibald) to proofread this article, and he actually pointed out that there is an error in Chrome that currently displays the status as "fulfilled" instead of "resolved". Thanks to Mathias Bynens, it is now fixed in Canary! ???

Okay, now we know how to better control that fuzzy Promise object. But what is he used for?

In the previous introduction chapter, I showed an example of getting a picture, compressing the picture, applying a filter to the picture and saving it! Eventually, this became a messy nested callback.

Fortunately, Promise can help us solve this problem!

First, let's rewrite the entire code block so that each function returns a Promise instead of the previous function.

If the picture is loaded and everything is normal, let us use the loaded picture to resolve (resolve) promise.

Otherwise, if there is an error somewhere while loading the file, we will reject (reject) promise with the error that occurred.

Let's see what happens when we run this code in the terminal?

very cool! As expected, promise got the value after parsing the data.

But now? We don't care about the entire promise object, we only care about the value of the data! Fortunately, there are built-in methods to get the value of promise.

For a promise, we can use the 3 methods above it:

  • .then():called after a promise is resolved
  • .catch():called after a promise is rejected
  • .finally():Whether the promise is resolved or rejected is always called

The .then method receives the value passed to the resolve method.

The .catch method receives the value passed to the rejected method.

In the end, we have the value of (resolved) after promise was resolved, and we do n t need the entire promise object!

Now we can use this value to do whatever we want.


By the way, when you know that a promise is always resolve or always reject, you can write Promise.resolve or Promise.reject and pass in the desired reject or The value of resolvepromise`.

You will often see this syntax in the examples below.

In the case of getImage, in order to run them, we eventually had to nest multiple callbacks. Fortunately, the .then processor can help us accomplish this!

The result of .then itself is a promise. This means we can link any number of .then:the result of the previous then callback will be passed as a parameter to the next then callback!

In the getImage example, in order to pass the processed image to the next function, we can chain multiple then callbacks.

Compared to the many nested callbacks we ended up with, we now have a neat then chain.

perfect! This syntax looks much better than the previous nested callbacks.

Macrotask and microtask

We know how to create promise and how to extract the value of promise.

Let's add some more code to the script and run it again:

Wait, what happened? !

First, Start! Is output.

Ok, we have already seen the upcoming message:console.log('Start!') Is output on the front line!

However, the second printed value is End!, Not the resolved value of promise! Only after End! Is printed will the value of promise be printed.

What happened here?

We finally saw the real power of promise! Although JavaScript is single-threaded, we can use Promise to add asynchronous tasks!

Wait, haven't we seen this before?

In JavaScript Event Loop , can we also use browser-native methods such as setTimeout to create some kind of asynchronous behavior?

Yes! However, inside the event loop, there are actually two types of queues:macro task queues(or just called task queues) and micro task queues.

(Macro) The task queue is used for macro tasks, and the micro task queue is used for micro tasks.

So what is a macro task and what is a micro task?

Although they are more than I introduced here, the most commonly used ones are already shown in the table below!

(Macro) task: setTimeout setInterval setImmediate
Microtask: process.nextTick Promise callback queueMicrotask

We see Promise in the list of microtasks! When a Promise resolves( resolve) and calls its then(), catch() or finally() methods, the callback functions in these methods are added to the microtask queue!

This means that the callback function in the then(), catch() or finally() method is not executed immediately, essentially adding some asynchronous behavior to our JavaScript code!

So when is the callback inside then(), catch(), or finally() executed?

The event loop gives different priorities to tasks:

  1. All functions currently in the call stack will be executed. When they return a value, they will be popped from the stack.
  2. When the call stack is empty, all queued microtasks will be popped from the microtask task queue into the call stack one by one, and then executed in the call stack!(The microtask itself can also return a new microtask, effectively creating an infinite microtask cycle)
  3. If both the call stack and the microtask queue are empty, the event loop will check if there are any tasks in the macrotask queue. If there are tasks in the macro task, they will be popped from the macro task queue into the call stack, and will be popped from the call stack after being executed!

Let's take a quick look at a simple example:

  • Task1:The function that was added to the call stack immediately, such as calling it immediately in our code.
  • Task2, Task3, Task4:Microtasks, such as callbacks in the then method in promise, or a task added with queueMicrotask.
  • Task5, Task6:Macro tasks, such as callbacks in setTimeout or setImmediate

First, Task1 returns a value and pops from the call stack. Then, the JavaScript engine checks the tasks queued in the microtask queue. Once all tasks in the microtask are put on the call stack and finally popped, the JavaScript engine will check the tasks in the macro task queue, pop them into the call stack and pop them off the call stack when they return a value.

The pink box in the picture is a different task, let's use it with some real code!

In this code, we have the macro task setTimeout and the micro task promise's then callback.

Once the JavaScript engine reaches the line where the setTimeout function is located, the event loop is involved.

Let's run this code step by step and see what kind of logs we will get!

Quick mention:In the example below, methods like console.log, setTimeout and Promise.resolve are being added to the call stack. They are internal methods that don't actually appear in the stack trace, so if you are using a debugger, don't worry, you won't see them anywhere. It just makes the concept easier to explain without adding a bunch of sample file code.

On the first line, the JavaScript engine encountered theconsole.log()method, which was added to the call stack, after which it output the value Start! In the console. The console.log function pops up from the call stack, after which the JavaScript engine continues to execute code.

The JavaScript engine encountered the setTimeout method and he was popped into the call stack. setTimeout is the browser's native method:its callback function(() => console.log('In timeout')) will be added to Web API until the timer finishes counting. Although the value we provide for the timer is 0, the callback will be pushed into the Web API first after it is added to the macro task queue( setTimeout is a macro task).

The JavaScript engine encountered the Promise.resolve method. Promise.resolve is added to the call stack. After Promise resolves( resolve) value, its callback function in then is added to the microtask queue.

The JavaScript engine sees that the call stack is now empty. Since the call stack is empty, it will check if there are queued tasks in the microtask queue! Yes, there are tasks in the queue, and the callback function in then of promise is waiting for its turn! It is popped into the call stack, and after that it outputs the value of promise( resolved):the string Promise! In this example.

The JavaScript engine sees that the call stack is empty, so if the task is queued, it will check the microtask queue again. At this point, the microtask queue is completely empty.

It's time to check the macro task queue:the setTimeout callback is still waiting there! setTimeout was popped into the call stack. The callback function returns the console.log method, which outputs the stringIn timeout!. The setTimeout callback is popped from the call stack.

Finally, everything is done! It looks like the output we saw before is not so unexpected in the end.

Async/Await

ES7 introduces a new way to add asynchronous behavior in JavaScript and makes promise easier to use! With the introduction of the async and await keywords, we can create an async function that implicitly returns a promise. But what should we do?

Earlier, we saw that whether we entered new Promise(() => {}), Promise.resolve or Promise.reject, we could explicitly create a promise using Promise object.

We can now create asynchronous functions that implicitly return an object instead of explicitly using the Promise object! This means that we no longer need to write any Promise objects.

Although it is a great fact that the async function implicitly returns promise, you can only see the true power of the async function when using the await keyword. When we wait for the value after await to return a resolved promise, we can pause the asynchronous function through the await keyword. If we want to get the value of resolved promise, just like we used the then callback before, we can assign a variable to the value of await promise!

So, can we pause an asynchronous function? Great, but what does that mean?

When we run the following code block let us see what happened:

Uh, what happened here?

First, the JavaScript engine encountered console.log. It is popped into the call stack, after which Before function! Is output.

Then, we call the asynchronous function myFunc(), after which the myFunc function body runs. In the first line of the function body, we call another console.log, this time the incoming string isIn function!. console.log is added to the call stack, the value is output, and then popped from the stack.

The function body continues to execute, taking us to the second line. In the end, we saw an await keyword!

The first thing to happen is to wait for the value to be executed:in this example, the function one. It is popped into the call stack, and eventually returns a "promise" in a resolved state. Once Promise is resolved and one returns a value, JavaScript encounters the await keyword.

When the await keyword is encountered, the asynchronous function is suspended. The execution of the function body is suspended, and the remaining code in the async function will run in a microtask instead of a regular task!

Now, because the await keyword is encountered, the asynchronous function myFunc is suspended, the JavaScript engine jumps out of the asynchronous function, and the code execution continues in the execution context in which the asynchronous function is called:in this example, it is global execution Context!

In the end, no more tasks are running in the global execution context! The event loop checks to see if any microtasks are queued:yes, there are! After solving the value of one, the asynchronous function myFunc starts to queue. myFunc is popped into the call stack and continues to run where it left off.

The variable res finally gets its value, which is the value of promise returned by one! We call console.log with the value of res(in this example, the string One!). One! Is printed to the console and console.log is popped from the call stack.

In the end, everything is done! Did you notice how the async function is different from then of promise? The await keyword suspends the async function, but if we use then, the body of Promise will continue to be executed!

Well, this is quite a lot of information! When using Promise, if you still feel a little overwhelmed, don't worry at all. I personally think that when using asynchronous JavaScript, you only need experience to pay attention to the pattern and feel confident.

When using asynchronous JavaScript, I hope that the "unpredictable" or "unpredictable" behavior you may encounter now becomes more meaningful!

At last

The way and style of language expressions of foreign friends' technical blogs are very different from those of Chinese people.

Whenever I see a long or sloppy sentence, I want to write an article in my own language ?

Maybe writing one by yourself is faster than translation ?

Recommended reading:

  1. Through 10 examples of small exercises to quickly get started with Vue3 core new features

  2. Heavy:A summary of 100K + Star front-end interview open source projects on GitHub(necessary for entering large factories)

  3. Highly recommended:170K + Star front-end learning data structure and algorithm project on GitHub

  4. A picture clarifies Vue 3.0's responsive system and implements a simplified version of the responsive system

  5. Black learning method:fairy skills for mining on GitHub-how to discover excellent open source projects

Support it ?