Editor’s note: As we all know, the biggest feature of JS is asynchronous, which improves performance but makes it difficult to write, creating a horrible callback hell. To solve the problem, one solution after another was put forward. Today, we invited the well-known translator of JavaScript Advanced Programming and other books, @Li Songfeng, to explain to us the solution of writing various asynchronous functions and various connotations.

Share before the content is based on the text version, if you want to look at the key words can be read before PPT:ppt.baomitu.com/d/fd045abb before you can also view the share video: cloud.live.360vcloud.net/theater/pla…


ES7 (ECMAScript 2016) introduces Async /await functions to control asynchronous processes by writing sequential and synchronous code, completely solving the problem of “callback hell” that has plagued JavaScript developers. For example, the asynchronous logic that previously required nested callbacks:

const result = [];
// pseudo-code, ajax stand for an asynchronous request
ajax('url1'.function(err, data){
    if(err) {... } result.push(data) ajax('url2'.function(err, data){
        if(err) {... } result.push(data)console.log(result)
    })
})
Copy the code

Now you can write synchronous code like this:

async function example() {
  const r1 = await new Promise(resolve= >
    setTimeout(resolve, 500.'slowest'))const r2 = await new Promise(resolve= >
    setTimeout(resolve, 200.'slow'))return [r1, r2]
}

example().then(result= > console.log(result))
// ['slowest', 'slow']
Copy the code

Async functions need to prefix the Async keyword with the function and internally “block” the asynchronous operation with the await keyword until the asynchronous operation returns the result, and then continue. Before Async, we could not imagine the following asynchronous code could get the result directly:

const r1 = ajax('url')
console.log(r1)
// undefined
Copy the code

This is of course impossible; the result of an asynchronous function can only be found in a callback. Async functions are, so to speak, the result of the efforts of JavaScript programmers to “save themselves” — not “candy” — from the pitfalls of efficient asynchronous programming. However, in case you don’t know, Async is actually a syntactic candy. Behind the Promise, Iterator, and Generator introduced in ES6 (ECMAScript 2015), we simply refer to PIG. This article gives you a good taste of this syntactic sugar and how a PIG makes Async.

1. Current JavaScript programming is mostly asynchronous programming

Currently JavaScript programming is mostly asynchronous programming. Why do you say that? Web development, or Web development, has been moving towards a more interactive era since Ajax became popular in 2005. In particular, with the popularity of SPA (Single Page Application), there was a suggestion that Web pages should be transformed into Web apps and compete with native apps. Angular, React, and Vue, born in the context of componentization of front-end development, are the result of further evolution of SPA.

What does the increasingly interactive nature of Web applications or development mean? This means that in accordance with the runtime nature of the browser, the primary JavaScript related tasks during the first load of a page are loading the base runtime and extension libraries (including scripts to patch older browsers), and then initializing and setting the state of the page. After the first load, user operations on the page, data I/O, and DOM updates are all managed by asynchronous JavaScript scripts. So the biggest use of JavaScript programming today is Web interaction, and asynchronous logic is at the heart of Web interaction.

However, prior to ES6, the only means of controlling asynchronous flows in JavaScript were events and callbacks. For example, the following example shows sending an asynchronous request through a native XMLHttpRequest object and registering the onLoad and onError events with success and error handlers, respectively:

var req = new XMLHttpRequest();
req.open('GET', url);

req.onload = function () {
    if (req.status == 200) { processData(req.response); }}; req.onerror =function () {
    console.log('Network Error');
};

req.send(); 
Copy the code

The following code shows the classic “error first” callback of Node.js. It’s important to note, however, that this functional programming Style is also called CPS for Continuation Passing Style, which I translate to “subsequent Passing Style.” This is because calling readFile passes in a callback function that represents subsequent operations. I’m not going to expand this one.

// Node.js
fs.readFile('file.txt'.function (error, data) {
    if (error) {
       // ...
    }
    console.log(data); });Copy the code

There are a number of problems with events and callbacks, mainly that they only work in simple cases. As logic becomes complex, the cost of writing and maintaining code multiplies. For example, it’s known as “callback hell.” More importantly, the asynchronous nature of the callback pattern is at odds with the synchronous, sequential human mindset.

In response to increasingly complex asynchronous programming requirements, ES6 introduced promises that address these issues.

2. Promise

Promise is commonly understood as “a Promise is a placeholder for future values”. That is to say, semantically speaking, a Promise object represents a “Promise” to a future value. If such Promise is taken “fulfill”, it will “resolve” to a meaningful data. If you reject, it “resolves” into a “rejection reason” which is an error message.

The state of the Promise object is very simple. The state is pending when it is born. When it is fulfilled in the future, the state will become a pity. A: Yes, I have rejected. This is a big pity and rejected, which is settled in September. The above state transitions are irreversible, so Promise is simple and easy to control, haha.

Here are all the apis associated with Promise. The first three are used to create Promise objects (see example below), the first two of the last four are used to register response functions (see example below), and the last two are used to control concurrency and preemption:

Here’s how to create a Promise instance through the Prmoise(Executor) constructor: To pass an executor, which takes two arguments resolver and rejector, resolve and reject, respectively. This changes the status of the new object from Pending to fulfilled and Rejected, and returns fulfillment and rejection, respectively. Of course, both resolve and Reject are called in the callback of an asynchronous operation. After the call, the event-loop scheduling mechanism in the runtime environment (the browser engine or Node.js’s Libuv) adds the associated response function — the fulfill response function or reject response function — and the associated parameters to the microtask queue for execution by the JavaScript thread on the next “tick”.

As mentioned earlier, when the state of the Promise object changes from pending to fulfilled, the fulfillment reaction is performed; Instead of Rejected, you execute a rejection reaction. The normal way is to register the cash function with p.teng () and the reject function with p.catch(), as shown in the following example:

p.then(res= > { // Cash the reaction function
  // res === 'random success'
})
p.catch(err= > { // Reject the reaction function
  // err === 'random failure'
})
Copy the code

There are also unconventional ways, and sometimes unconventional ways are better:

// Register both the cash and reject functions through a.then() method
p.then(
  res= > {
    // handle response
  },
  err => {
    // handle error})// Only one function is registered via the.then() method: the implementation function
p.then(res= > {
  // handle response
})
Then () passes only the reject function, passing null to the position of the function
p.then(null, err => {
  // handle error
})
Copy the code

So much for promises. In addition to promises, ES6 also introduces iterators and generators, thus making PIG combinations of Async functions possible. Let’s take a quick look at Iterator and Generator respectively.

3. Iterator

The easiest way to understand Iterator or Iterator is to look at its interface:

interface IteratorResult {
  done: boolean;
  value: any;
}
interface Iterator {
  next(): IteratorResult;
}
interface Iterable {
  [Symbol.iterator](): Iterator
}
Copy the code

Start with the Iterator in the middle.

What is an iterator? It is an object with a next() method that returns an IteratorResult each time it is called (see the first interface IteratorResult). The iterator result is also an object with two attributes: done and value, where done is a Boolean value and false indicates that the iterator iterates through a sequence without ending. True indicates the end of the iterator’s iteration sequence. Value is what the iterator actually returns each time it iterates.

The final interface Iterable, translated as “Iterable,” has a [symbol.iterator]() method that returns an iterator.

Can combine the front interface definition and the graphic below understanding object iteration (realized “iterative agreement”), the iterator (implements “iterator agreement”) and the result of the iterator the three simple but important concept (behind temporarily don’t understand it doesn’t matter, and an example of an infinite sequence, can help you understand).

Iterables are a familiar concept. Arrays, strings, and the new collection types Set and Map in ES6 are all iterables. What does that mean? This means we can use E6’s three new syntax for manipulating iterables:

  • for... of
  • [...iterable]
  • Array.from(iterable)

Note that the following syntax prior to E6 does not apply to iterables:

  • for... in
  • Array#forEach

Now let’s look at examples.

for (const item of sequence) {
  console.log(item)
  // 'i'
  // 't'
  // 'e'
  // 'r'
  // 'a'
  // 'b'
  // 'l'
  // 'e'
}

console.log([...sequence])
// ['i', 't', 'e', 'r', 'a', 'b', 'l', 'e']

console.log(Array.from(sequence))
// ['i', 't', 'e', 'r', 'a', 'b', 'l', 'e']
Copy the code

The above examples use for… Of, extension operators (…) And array.from () to iterate over the previously defined sequence.

Let’s look at a small example of creating an infinite sequence using iterators to further understand the concepts associated with iterators.

const random = {
  [Symbol.iterator]: (a)= > ({
    next: (a)= > ({ value: Math.random() })
  })
}

// What happens when you run this line of code?
[...random]
// What about this line?
Array.from(random)
Copy the code

This example uses two ES6 arrow functions to define two methods and create three objects.

The innermost object {value: math.random ()} is clearly an “IteratorResult” object because it has a value attribute and a… Wait, what about the done attribute? The done attribute is not defined here, so iteratorresult.done returns false for each iteration (calling next()); So the definition of the iterator result is equivalent to {value: math.random (), done: false}. Obviously, done can never be true, so this is an infinitely random sequence of numbers!

interface IteratorResult {
  done: boolean;
  value: any;
}
Copy the code

Looking out further, the arrow function that returns the result object of this iterator is assigned to the next() method of the outer object. According to the Iterator interface, what is an object that contains a next() method whose return value is an Iterator? That’s right, iterators. Ok, the second object is an iterator!

interface Iterator {
  next(): IteratorResult;
}
Copy the code

Looking out further, the arrow function that returns this iterator object is assigned to the [symbol.iterator]() method of the outer object. According to the Iterable interface, what is an object that contains a [symbol.iterator]() method whose return value is an iterator? Yes, iterables.

interface Iterable {
  [Symbol.iterator](): Iterator
}
Copy the code

Ok, by now we should have a thorough understanding of iterators and related concepts. Let’s continue with the example. The previous example defines an iterable, random, whose iterator can return random numbers indefinitely, so:

// What happens when you run this line of code?
[...random]
// What about this line?
Array.from(random)
Copy the code

Yes, both lines of code cause the program (or runtime) to crash! Because iterators run endlessly, blocking the JavaScript execution thread, they can eventually stop responding at runtime, or even crash, as they fill up the available memory.

So what is the correct way to access an infinite sequence? The answer is to use a deconstruction assignment or to give for… The of loop sets the exit condition:

const [one, another] = random  // Parse the assignment to get the first two random numbers
console.log(one)
/ / 0.23235511826351285
console.log(another)
/ / 0.28749457537196577

for (const value of random) {
  if (value > 0.8) { // Exit condition, the loop is interrupted if the random number is greater than 0.8
    break
  }
  console.log(value)
}
Copy the code

Of course, there are more advanced ways to use infinite sequences, which I won’t cover here for the purposes of this article. Let’s talk about the last ES6 feature Generator.

4. Generator

For example, the upper interface:

interface Generator extendsIterator { next(value? :any): IteratorResult;
    [Symbol.iterator](): Iterator;
    throw(exception: any);
}
Copy the code

Can you tell me what a baby is? From its interface alone, it is both an iterator and an iterable. Yes, generators are therefore “enhanced” versions of iterators. Why? Because the generator also provides the yield keyword, the sequence value it returns is automatically wrapped in an IteratorResult object, saving us the trouble of writing the corresponding code by hand. Here is the definition of a generator function:

function *gen() {
  yield 'a'
  yield 'b'
  return 'c'
}
Copy the code

The generator of the interface definition is an object, not a function.

In fact, it’s not exactly true that generators are objects or functions. But we know that calling a generator function returns an iterator (the object that the interface describes) that controls the logic and data encapsulated by the generator function that returns it. In this sense, a generator consists of two parts: the generator function and the iterators it returns. In other words, generator is a general concept, a catch-all. (Don’t worry, you’ll see the point of this understanding of generators in a moment.)

At the beginning of this section, we said that a generator (the object returned) is “both an iterator and an iterable.” Let’s verify it:

const chars = gen()
typeof chars[Symbol.iterator] === 'function' // Chars is an iterable
typeof chars.next === 'function'  // chars is an iterator
chars[Symbol.iterator]() === chars  // The iterator of chars is itself
console.log(Array.from(chars))  // You can use array.from on it
// ['a', 'b']
console.log([...chars]) // You can use array.from on it
// ['a', 'b']
Copy the code

We get all the answers through comments in the code. Here’s a quick question: “Why does iterating over this generator return a sequence value that doesn’t contain the character ‘C’?”

The reason is that both ‘a’ and ‘b’ are valid sequence values because the done property of the iterator result object returned by yield is false; The done attribute is true. True indicates the end of the sequence, so ‘c’ is not included in the iterator result. If there is no return statement, the code will implicitly return {value: undefined, done: true} at the end of the generator function. I don’t have to tell you to believe that.

This is just one aspect of generators as “enhanced” iterators. Next, we get to the other side of the generator’s true power!

What’s really powerful about a generator, and what differentiates it from iterators, is that it can not only return values on each iteration, but it can also receive values. (Of course, generator functions are inherent in the concept of generators. Of course functions can accept arguments. Wait, not only can you pass parameters to generator functions, but you can also pass parameters to yield expressions!

function *gen(x) {
  const y = x * (yield)
  return y
}

const it = gen(6)
it.next()
// {value: undefined, done: false}
it.next(7)
// {value: 42, done: true}
Copy the code

In the simple generator example above. We define a generator function *gen() that takes an argument x. There is only a yield expression inside the function, as if nothing is being done. However, the yield expression appears to be a “placeholder for the value,” because the code at some point evaluates the product of the variable X and the “value” and assigns the product to the variable Y. Finally, the function returns y.

This is a little confusing, so let’s do it step by step.

  1. callgen(6)Creates iterators for generatorsit(As mentioned earlier, the generator contains the iterator and the generator function that returns it), passing in the number 6.
  2. callit.next()Start the generator. At this point the generator function’s code executes to the firstyieldPauses at the expression and returnsundefined. (yieldIt’s not idle, it looks at the value that’s not explicitly returned, and it returns the default valueundefined).
  3. callit.next(7)The recovery generator executes. At this timeyieldUpon receiving the passed number 7, immediately resume execution of the generator function code and replace itself with the number 7.
  4. Code calculation:6 * 7, gets 42, and assigns 42 to the variabley, and finally returnsy.
  5. The final value returned by the generator function is:{value: 42, done: true}.

In this example, there is only one yield. If there are more yields, step 4 suspends the generator function again at the second yield, returning a value, and then steps 3 and 4 repeat, passing a value to the generator function by calling it.next() again.

To recap, each call to it.next() causes the generator to pause or stop execution in four ways:

  1. yieldThe expression returns the next value in the sequence
  2. returnStatement returns the value of the generator function ({ done: true })
  3. throwStatement completely stops generator execution (more on that later)
  4. At the end of the generator function, an implicit return is made{ value: undefined, done: true}

Note that return and throw can be called either inside a generator function or outside of a generator function via iterators such as it.return(0), it.throw(new Error(‘Oops’)). We’ll give you an example of that later.

From this, we can see that what makes a generator special is its yield keyword. This yield is interesting in two ways: first, it is the point at which generator functions pause and resume execution; Second, it is a medium for passing values (including errors/exceptions) both outward and inward.

Speaking of errors/exceptions, let’s focus on how generators handle exceptions. After all, error handling is one of the biggest headaches for JavaScript programmers when writing asynchronous code using callbacks.

4.1 Synchronization Error Handling

First, we look at “inside-out” error passing, where errors are thrown into iterator code from within a generator function.

function *main() {
  const x = yield "Hello World";
  yield x.toLowerCase(); // Cause an exception!
}

const it = main();
it.next().value; // Hello World
try {
  it.next( 42 );
} catch (err) {
  console.error(err); // TypeError
}
Copy the code

As the code comments indicate, the second line of the generator function causes an exception (the reader is free to run the code himself and deduce why). Because no exception handling is done inside the generator function, the error is thrown to the generator’s iteration code, which is the line it.next(42). Fortunately, this line of code is wrapped in a try/catch, and the error can be caught and handled normally.

Next, look at “outside-in” (or, more accurately, “outside-in and outside-out”) misdelivery.

function *main() {
  var x = yield "Hello World";
  console.log('never gets here'); 
}

const it = main();
it.next().value; // Hello World
try {
  it.throw('Oops'); // '*main()'
} catch (err) {   / / not!
  console.error(err); // Oops
}
Copy the code

As shown in the code, the iterating code throws an exception through it.throw(‘Oops’). This exception is thrown into the generator function (via the iterator it). Once thrown in, the yield expression finds itself with a “hot potato” and sees no exception handling logic around it, so it quickly throws the exception again. The iterator it is clearly prepared to see if there is any logic inside the generator function to handle exceptions (see the comment “// * does main() handle exceptions?”). “), “No!” It has long waited for its own try/catch.

4.2 Asynchronous iterative generator

The iterative passing of values to the generator, including passing errors, as we saw earlier, is synchronous. In fact, what’s really (oh, another “really”) powerful about the yield expression for generators is this: Instead of having to wait for iterator code to return a value after suspending generator code execution, it can have the iterator get the return value in a callback to an asynchronous operation, and then pass the value to it via it.next(res).

Is that clear? Yield can wait for the result of an asynchronous operation. Thus making possible the seemingly impossible situation mentioned at the beginning of this article:

const r1 = ajax('url')
console.log(r1)
// undefined
Copy the code

Add a yield before an asynchronous operation:

const r1 = yield ajax('url')
console.log(r1)
// this time r1 is the actual response
Copy the code

It’s better to use an asynchronous operation that returns a Promise as an example. Because callback-based asynchronous operations can easily be converted to promise-based asynchronous operations (such as $.ajax() in jQuery or asynchronous methods in Node.js converted to promises via util.promisify).

Here’s an example. This is an example of a pure Promise.

function foo(x,y) {
  return request(
    "http://some.url.1/? x=" + x + "&y=" + y
  );
}

foo(11.31)
  .then(
    function(text){
      console.log(text);
    },
    function(err){
      console.error(err); });Copy the code

The function foo(x, y) encapsulates an asynchronous request and returns a Promise. Once foo(11, 31) is passed in, Request sends a request to the concatenated URL, returning a Promise object in its pending state. If the request succeeds, then() executes the implementation function registered in then() to process the response; If the request fails, the reject response function is executed and an error is processed.

The next step is to combine the above code with a generator that focuses only on sending the request and getting the result of the response, while abstracting the wait and callback processing logic for asynchronous operations as implementation details. (” As details. “Yes, our goal is to focus only on the request and the result. The process is details.)

function foo(x, y) {
  return request(
    "http://some.url.1/? x=" + x + "&y=" + y
  );
}
function *main() {
  try {
    const result = yield foo(11.31);  // Asynchronous function call!
    console.log( result );
  } catch (err) {
    console.error( err ); }}const it = main(); 
const p = it.next().value; // Start the generator and get Promise 'p'

p.then( // Wait for Promise 'p' to resolve
  function(res){
    it.next(res);  // Pass 'text' to '*main()' and resume execution
  },
  function(err){
    it.throw(err);  // throw 'err' to '*main()'});Copy the code

Notice the asynchronous function call foo(11, 31) in the yield expression of the generator function (*main). All we have to do is get the Promise returned by the asynchronous function call through it.next() in the iterator code and process it correctly. How to deal with it? Let’s look at the code.

After the generator iterator is created, const p = it.next().value; Returns the Promise P. In p’s cashed response function, we return the response res to yield in the generator function via the it.next(res) call. Yield immediately resumes execution of the generator code, assigning res to the variable result. So we successfully get the response to the asynchronous request in synchronous code writing in the generator function! Magic is not?

(Of course, if an error occurs on an asynchronous request, the error is also thrown to the generator function via it.throw(err) in p’s rejection function. But that’s not important right now.

Ok, goal achieved: we have perfect control over asynchronous operations using the generator’s synchronous code. However, there is a problem. The generator in the example above only wraps one asynchronous operation. What if there are multiple asynchronous operations? At this point, it’s a good idea to have a generic code for handling generator functions that automates the “details” of Promise acceptance, waiting, and response/error passing, no matter how many asynchronous operations are involved.

Isn’t that just a generator running on Promise?

5. General-purpose generators run programs

To sum up, what we want is this:

function example() {
  return run(function* () {
    const r1 = yield new Promise(resolve= >
      setTimeout(resolve, 500.'slowest'))const r2 = yield new Promise(resolve= >
      setTimeout(resolve, 200.'slow'))return [r1, r2]
  })
}

example().then(result= > console.log(result))
// ['slowest', 'slow']
Copy the code

That is, to define a generic run function that handles any number of asynchronous operations wrapped in its generator functions. For each operation, it correctly returns an asynchronous result or throws an exception into a generator function. The end result of running this function is to return a Promise containing the results of all asynchronous operations returned by the generator function (as in the above example).

There are already smart people who have implemented such a running program, so here are two implementations that you can try to run for yourself, and then “human flesh” implementation to better understand.

Note that before ES7 introduced Async, callback-plagued JavaScript programmers relied on similar run-offs combined with generators to keep themselves alive. In fact, in the “wild west” days before ES6 (no Promises, no generators), persistent and resourceful JavaScript programmers had already figured out/found implementations of Thenable (a precursor to Promise) and similar generators (such as ReGenerator), Enabling browsers to support their own productivity dreams of writing asynchronous code in a synchronous style.

Bitter zai! Wei zai! Sad husband, wrung xi hu!

This is one:

function run(gen) {
  const it = gen();
  return Promise.resolve()
    .then( function handleNext(value){
      let next = it.next( value );
      return (function handleResult(next){
        if (next.done) {
          return next.value;
        } else {
          return Promise.resolve( next.value )
            .then(
              handleNext,
			  function handleErr(err) {
                return Promise.resolve( it.throw( err ) ) .then( handleResult ); }); }// if... else
      })(next); // handleResult(next)
    }); // handleNext(value)
}
Copy the code

For reference “human flesh” implementation process

(See the code that calls run at the beginning of this section.)

The run function takes a generator function as an argument and immediately creates the generator iterator it (see the run function code above).

It then returns a Promise, created directly through promise.resolve ().

Our.then() method for this Promise passes in an implementation response function (which must be called because the Promise is fulfilled) called handleNext(Value), which takes a value argument. The first time it is called, no value is passed in, so the value of value is undefined.

Next, the first call to it.next(value) starts the generator, passing undefined. The generator’s first yield returns a Promise of a pending state, which won’t be resolved until at least 500ms later.

The value of the next variable is {value: < Promise [pending]>, done: false}.

Next, pass the next to the IIFE (Immediately Invoked Function Expression, invokes the Function Expression Immediately), which is called handleResult (Handles the result).

Inside handleResult(Next), first check next. Done, not true, and enter the else clause. Resolve (Next-value) wrap next. Value using promise.resolve (Next-value), which is used to wait for the returned promises to be resolved, and grammar of string values’ vs’, which are transmitted to handleNext(value).

At this point, the first half of the first asynchronous operation is processed. Next, handleNext(value) is called again, passing the string ‘grammar’. Iterators again call next(value) to grammar back the first yield in generator functions, which yields the string and immediately resumes generator execution, assigning the string to the variable R1. The code in the generator function continues and pauses at the second yield, at which point a second Promise with a final value of ‘slow’ is created and returned, but the Promise is undetermined and will not be resolved until 200 milliseconds later.

Continuing, in the iterator code, the variable next again gets an object {value: , done: false}. Go to IIFE again and pass next. Check that next.done does not equal false and wrap next.value into a promise.resolve (next.value) in the else block…

Look, here’s another one:

function run(generator) {
  return new Promise((resolve, reject) = > {
    const it = generator()
    step((a)= > it.next())
    function step(nextFn) {
      const result = runNext(nextFn)
      if (result.done) {
        resolve(result.value)
        return
      }
      Promise
        .resolve(result.value)
        .then(
          value= > step((a)= > it.next(value)), 
          err => step((a)= > it.throw(err))
        )
    }
    function runNext(nextFn) {
      try {
        return nextFn()
      } catch (err) {
        reject(err)
      }
    }
  })
}
Copy the code

6. Why Async functions are syntactic sugar

With this runfunction in hand, we can compare the following two example() functions:

The first example() controls asynchronous code by running a program through a generator; The second example() is an Async function that controls asynchronous code with Async /await.

The only difference is that the former has an extra layer of run function encapsulation, uses yield instead of await, and does not have the async keyword modifier. Other than that, the core code is exactly the same!

Now, when you see an asynchronous function like the following, what do you think?

async function example() {
  const r1 = await new Promise(resolve= >
    setTimeout(resolve, 500.'slowest'))const r2 = await new Promise(resolve= >
    setTimeout(resolve, 200.'slow'))return [r1, r2]
}

example().then(result= > console.log(result))
// ['slowest', 'slow']
Copy the code

Yes, Async functions or Async /await are a bitter and sweet syntax-candy built on Promise, Iterator and Generator! Remember Async function = Promise + Iterator + Generator, or “Async function turned out to be PIG”.

7. Reference materials

  • ECMAScript 2018
  • Practical Modern JavaScript
  • You Don’t Know JS: Async & Performance
  • Understanding ECMAScript 6
  • Exploring ES6