This is the second day of my participation in Gwen Challenge.

preface

There are lots of articles on the Internet about async/await, I’m sure you have read a lot of articles, so why am I writing articles like this? Because… It is similar to the article read many maybe you learn to na (shameless! . Ok, let’s get back to the point, we use async/await a lot in our daily development because it’s so easy to handle asynchronous operations. So how does it come about and what are the underlying principles? This article will be from the history of its origin and underlying principles of these two aspects, if you have a harvest, welcome to click a thumbs up.

What problem does async/await solve

I’m not going to answer that, but let’s take a look at four scenarios.

Bronze Player: Callback “Callback Hell”

As we all know, JavaScript is a single-threaded language. Before ECMAScript 2015, asynchronous operations such as Ajax and scheduled tasks were handled by callback functions. Asynchronous tasks in the queue were executed by JS after the call stack was emptied.

The Call stack is essentially a JS interpreter, such as V8, for tracking code execution. See the Call Stack for details.

Suppose we now have a logic: for example, get an ID through Ajax, if id is 1, then get the user name, if zhangsan, then get his address information address. What happens if you use a callback function?

Start by wrapping a simple Ajax:

/** * Ajax easily encapsulates GET requests *@param {string} Url Request address *@param {function} Cb callback function */
function ajax(url, cb) {
  let res
  const xhr = new XMLHttpRequest()
  xhr.onreadystatechange = handleReadyStateChange
  xhr.open('GET', url)
  xhr.send()

  function handleReadyStateChange() {
    if (this.readyState === this.DONE) {
      if (this.status === 200) {
        cb(null.JSON.parse(this.responseText))
      } else {
        cb(new Error('error')}}}}Copy the code

Then create three JSON files a. son, B. son and C. son and put them in the data directory. Let’s implement the above requirements:

  <script src="./ajax.js"></script>
  <script>
    ajax('./data/a.json'.(err, d) = > {
      if (err) {
        console.log(err);
      } else {
        if (d.id === 1) {
          ajax('./data/b.json'.(err, d) = > {
            if (err) {
              console.log(err);
            } else {
              if (d.name === 'zhangsan') {
                ajax('./data/c.json'.(err, d) = > {
                  if (err) {
                    console.log(err);
                  } else {
                    console.log(d); }})}}})</script>
Copy the code

At this point, you will find that the code is walking sideways like a crab. In fact, it is known as callback hell.

Gold player: Promise “chain call”

Fortunately, an asynchronous solution has emerged in the community: Promise. The earliest modules include WHEN and Bluebird, which were later written into the ECMAScript 2015 specification. Its appearance has greatly accelerated the development process of the front end.

In the latest edition of the Little Red Book (4th edition) it is translated as a covenant.

The early dated mechanism was in the form of the Deferred API in jQuery and Dojo. By 2010, the CommonJS project was implementing Promises/A specifications that were gaining popularity. Third-party JavaScript scheduling libraries such as Q and Bluebird are also gaining community acceptance, although their implementations are somewhat different. In order to bridge the gap between existing implementations, in 2012 Promises/A+ forked the CommonJS Proposals for Promises/A and created A specification for Promises/A+ under the same name. This specification eventually became the model for the implementation of the ECMAScript 6 specification.

Promise is a container with states, and there are only three kinds: fullfilled, Rejected and pending.

The container’s state is determined by the operation inside it, and we don’t need to worry about when the container’s state changes. Just know one thing: The container’s state is guaranteed to change to fullfilled and Rejected in the future. Once the container’s state changes, it will never change again. We can get the result through instance methods such as THEN and catch.

Going back to the previous example, now Promise, we can put Ajax operations into this container and map the success and failure of the request to the state of the container, shifting our focus from callback functions to the state. Here’s how to write it.

Ajax becomes a stateful “container” wrapped in Promise:

function ajax(url) {
  return new Promise((resolve, reject) = > {
    const xhr = new XMLHttpRequest()
    xhr.onreadystatechange = handleReadyStateChange
    xhr.open('GET', url)
    xhr.send()

    function handleReadyStateChange() {
      if (this.readyState === this.DONE) {
        if (this.status === 200) {
          resolve(JSON.parse(this.responseText))
        } else {
          reject(new Error('ajax error')}}}})}Copy the code

The way requests are made has also changed:

ajax('./data/a.json')
  .then(d= > {
    if (d.id === 1) return ajax('./data/b.json')
  })
  .then(d= > {
    if (d.name === 'zhangsan') return ajax('./data/c.json')
  })
  .then(d= > {
    console.log(d);
  })
  .catch(e= > {
    console.log(e);
  })
Copy the code

Yi? We found no, it is not “horizontal walk”, but “vertical walk” 🤣.

If you’ve ever worked with D3.js or jQuery, you’ll be familiar with the above notation, which is called “chain call”. It’s essentially a callback function, calling then when the Promise state changes to fullfilled, and calling catch when the Promise state fails.

The second argument to the then method can also accept failed callbacks, but catch can catch all returned Promise exceptions, whereas the former can only catch the last exception, so it is generally not recommended to use catch instead.

Although players than bronze “callback hell”, “chain called” seems clear, but it looks like a big then I still feel brain broadly pain, code semantic also not clear, because all the logical judgment in then, still did not reach the ideal purpose, we hope in a synchronous writing to write asynchronous code, Making the code not only clear but also easy to write, it’s clear that promises are still too low. The APIGenerator function, also in ES6, has changed that, and here’s how.

Diamond Player: Generator” Synchronous writing”

Given that not everyone knows about Generator, LET me briefly describe its syntax.

Generator is a Generator function. A call to this function does not immediately execute the function, but generates an Iterator object that must be called to the next method of the Iterator object. It does not execute all at once. If the yield keyword is encountered during execution, the function is paused until the next method is called.

The implementation of an iterator needs to conform to the iterative protocol, and the iterator object returned by the Generator is an implementation. See the iterative protocol on the MDN for details.

It is easy to define a generator function by adding * to the function name. If the function does not contain the yield keyword, it is not much different from a normal function, so it is usually used with the yield keyword.

function* gen(x) {
    let y = yield x
    yield y + 1
}

const g = gen(1) 
g.next() // {value: 1, done: false}
g.next(2) // {value: 3, done: false}
g.next() // {value: undefined, done: true}
Copy the code

We can see that calling next returns an object with two properties: value, the value after the yield keyword, and done, to indicate whether the traversal is complete. In terms of data structure, the g traverser object looks like a pointer to a singly linked list. Calling next moves the pointer to return the current node. When moving to the last node of the singly linked list, the done property is naturally true, indicating that it is finished.

The next function can take an argument as the yield of the previous one, which I’ll explain in a moment.

Now that you have a basic understanding of the Generator function, let’s think about its use in an asynchronous scenario. If yield is followed by a Promise object, what will happen? Back to the example above, let’s rewrite it with Generator.

  function* getData() {
    const res = yield ajax('./data/a.json')
    if (res.id === 1) {
      const res = yield ajax('./data/b.json')
      if (res.name === 'zhangsan') {
        const res = yield ajax('./data/c.json')
        console.log(res)
      }
    }
  }
Copy the code

Here’s the trouble. As stated above, the execution of Generator functions calls next and is paused at yield until the next call, but in the above requirement we need to know the value returned by the first execution before deciding whether to execute the next asynchronous request. After the first call to next, the value of res is undefined. How do I get the result of the last asynchronous operation?

The next function returns the last value by passing the last value as an argument.

  const gen = getData()

  gen.next().value.then(d= > {
    gen.next(d).value.then(d= > {
      gen.next(d).value.then(d= > {
        gen.next(d)
      })
    })
  })
Copy the code

We can wrap an executor and recurse automatically, using the done value as the condition to end the recursion.

  function run(gen) {
    const g = gen()

    function next(d) {
      const res = g.next(d)
      if (res.done) return res.value
      res.value.then((d) = > {
        next(d)
      })
    }

    next()
  }

  run(getData)
Copy the code

We don’t need to worry about the run function at all. Someone in the community has already written a module with more complete functions, as well as error handling, etc. It is a small tool called CO released by TJ Holowaychuk in June 2013. We just need to pay attention to the business logic code itself.

The CO module specifies that yield must be followed by a Thunk function or a Promise object.

King player: async/await ‘more elegant’

Thankfully, ECMAScript normalized the above scheme in 2017 and called it async/await, which corresponds to the * and yield in Generator functions, so the asynchronous programming solution has come to an end. Rising from bronze to king is not easy 😆.

Here are the changes to the final version:

  async function getData() {
    try {
      const res = await ajax('./data/a.json')
      if (res.id === 1) {
        const res = await ajax('./data/b.json')
        if (res.name === 'zhangsan') {
          const res = await ajax('./data/c.json')
          console.log(res)
        }
      }
    } catch (error) {
      console.log(error)
    }
  }

  getData()
Copy the code

Back to the problem itself: what problem does async/await solve?

I think you already have the answer in your mind. It’s not an accident. It’s a necessity. Async /await is a more elegant way of writing. It is a combination of Generator and Promise. Here are its advantages:

  • The built-inGeneratorAutomatic actuator of
  • Better semantics
  • The return value is Promise

Basic Principle Analysis

From the above four scenarios, we know that async/await is by far the most elegant way to solve asynchronous programming, but have you ever wondered:

  • inGeneratorInternally, how does it “pause” and then resume execution?
  • How does the JS engine know where the last execution was?

For the first question, which refers to the concept of a coroutine, a Generator function is an implementation of a coroutine, which can be explained as follows: A Generator function is a coroutine that gives execution to another coroutine at yield, and resumes execution from where it was suspended when the other coroutine has completed its task.

Here’s a quote from Wikipedia:

Coroutines (English: coroutine) are components of computer programs that promote cooperative multitasking subroutines that allow execution to be suspended and resumed. Coroutines are more general and flexible than subroutines, but are not as widely used in practice. Coroutines are better suited for implementing familiar program components such as cooperative multitasking, exception handling, event loops, iterators, infinite lists, and pipes.

“Allow execution to be suspended and resumed” is a good explanation.

So back to the second question, how does the JS engine know where the last execution was? That is, how does it go back to where it paused when it’s done with another coroutine?

Let’s go back to the example above, and let’s do it manually:

  function* getData() {
    const res = yield ajax('./data/a.json')
    if (res.id === 1) {
      const res = yield ajax('./data/b.json')
      if (res.name === 'zhangsan') {
        const res = yield ajax('./data/c.json')
        console.log(res)
      }
    }
  }
  
  const gen = getData()
Copy the code

In the browser console, print the object g:

Let’s focus on two properties: [[GeneratorLocation]] and [[GeneratorState]] are private properties that are exposed in the host environment and cannot be accessed by us. The former is to record the location of the current Generator function execution, while the latter is to record the state of the Generator function execution. The current state is suspended. Html.html is now suspended at line 12, so let’s check it out:

The pause is at the declared position of the function, and the next method is called once, and we see that it moves to the first line of the function, line 13 of the index.html file. When the next method is called four times, its state is now closed:

Now it’s back where it was declared, so that explains the second problem.

conclusion

This article starts with four different periods of asynchronous task handling, and whatever the “segment” solution is, they all drive the development of asynchronous programming. Callback functions that handle asynchrony are unfriendly, but they are the foundation of all solutions. The emergence of Promise introduced state. Although chained invocation made the code logic unclear, it solved the problem of “callback hell” brought by callback functions. Finally, the emergence of Generator functions and Promise made async/await and was considered as the best solution for asynchronous task processing in the industry.

I still believe in a word, the emergence of any technology must be to solve the problem of a certain scene, we in addition to “catch up with the fashion”, but also to know why it appears, so that when we encounter related problems in the future, they will become our “sword” to solve the problem.

I hope you found this article helpful.

reference

  • Asynchronous application of Generator functions
  • Async function
  • coroutines
  • Iterative agreement