This is the 20th day of my participation in the August More Text Challenge

📣 Hello everyone, I am Xiao Cheng, this article will show you how to understand and feel asynchronous programming in JavaScript

The introduction

Let’s start with a very common scenario: operating on data returned from the server

The interaction with the server side is an asynchronous operation

If you were writing normal code, you might write something like this

I don’t know what I’m typing, but it’s basically an asynchronous request that returns an assignment to data and says,

let data = ajax("http://127.0.0.1",ab) // Just write it
console.log(data)
Copy the code

Although the whole idea doesn’t seem wrong, right? However, it does not work. The data retrieval is asynchronous, which means that when the data is requested, the output has already been executed, which must be undefined

So why did it do it?

JavaScript is a single-threaded language. What would happen if asynchronous execution were eliminated

It’s like shopping. You have to follow the person in front of you. He stops to buy something and everyone behind him stops and waits for him to come back. Switching to JS is the same, blocking code execution. Hence the concept of asynchrony. Let’s first look at the concept of asynchrony and how asynchronous operations are traditionally implemented

What are synchronous and asynchronous

Synchronization: Tasks are executed sequentially. When a large number of time-consuming tasks are encountered, subsequent tasks will be delayed. This delay is called blocking

Asynchronous: It does not wait for time-consuming tasks, but immediately executes the next task after encountering asynchronous tasks. The subsequent logic of time-consuming tasks is usually defined by callback functions, and the code execution sequence is out of order

Implementing asynchronous programming

Prior to ES6, there were several approaches to asynchronous programming.

  1. The callback function
  2. Event listeners
  3. Publish/subscribe
  4. Promise object

Let’s start by reviewing how asynchronous programming is done in the traditional way

Callback

A callback function can be understood as a desired thing, defined by the caller, given to the executor at a certain time, put the required operation in the function, and pass the function to the executor to execute

This is mainly reflected in writing the second paragraph of the task in a function, which is called directly when the task is re-executed

Then one might ask, what is the second paragraph? Let’s take another example, reading a file and printing it. It must be asynchronous, but how can it be divided into two paragraphs?

According to the logical division, the first paragraph is to read the file, the second paragraph is to print the file, which can be understood as the first paragraph is to request data, the second paragraph is to print data

Ruan teacher’s code example

fs.readFile('/etc/passwd'.'utf-8'.function (err, data) {
  if (err) throw err;
  console.log(data);
});
Copy the code

At the end of the first phase, the result is returned to the following function as an argument, passing in the second segment

Callbacks are used in the following scenarios:

  1. Event callback

  2. Timer callback

  3. An Ajax request

Promise

There is no problem with the callbacks themselves, but the problem arises in the nesting of multiple callbacks

Think about it, I execute you, you execute him, he execute her…

Is it necessary to layer upon layer of nesting, that nesting doll operation is obviously not conducive to reading

fs.readFile(fileA, 'utf-8'.function (err, data) {
  fs.readFile(fileB, 'utf-8'.function (err, data) {
    // ...
  });
});
Copy the code

You can also think of it this way: if one of the codes needs to be changed, both its upper and lower callbacks need to be changed, which is also called strong coupling

Coupling, lotus root, correlation is very strong meaning

This scenario is also called “callback hell.”

The Promise object was created to solve this problem, using a new way of writing: chain calls

Promise can be used to indicate the execution state of an asynchronous task, and there are three states

  • Pending: Indicates that the start state is waiting
  • This is a big pity: the state of success will trigger onFulfilled
  • Rejected: Indicates the failed state. OnRejected is triggered

It is written as follows

const promise = new Promise(function(resolve, reject) {
    // Synchronize the code
    // resolve Execution indicates that the asynchronous task succeeds
    // reject Execution Indicates that asynchronous tasks fail
    resolve(100)
    // reject(new Error('reject')) // fail
})
promise.then(function() {
    // Successful callback
}, function () {
    // Failed callback
})
Copy the code

The Promise object calls the THEN method and returns a new Promise object, which can then continue the chain call

The subsequent THEN method registers the callback for the Promise object returned by the previous THEN

The return value of the callback function from the previous THEN method is used as an argument to the callback from the subsequent THEN method

The purpose of chain calls is to solve the problem of nested callback functions

More details on Promise won’t be covered here, but will be covered in the next article

Broken, broken, nested loops, I’m in callback hell, try to be more literal

Promise solved callback hell, and it wasn’t the ultimate solution to asynchronous programming, so what problems did it cause?

  1. Unable to cancel Promise
  2. When in a pending state, no progress is known
  3. Errors cannot becatch

But none of this is the biggest problem with Promise. Its biggest problem is code redundancy. When the execution logic becomes complicated, the semantics of the code become very unclear, and it’s all then

For those of you who have read the previous article, this should give you an idea of how Generator can implement asynchronous programming. It seems that the next method can start, run, and pass parameters as well as the then method

Generator

The Generator function can pause and resume execution, which is the fundamental reason it encapsulates asynchronous tasks. In addition, it has two characteristics that make it a perfect solution for asynchronous programming.

  • Data transfer in and out of a function
  • Error handling mechanism

The data transfer

Before we look at how asynchronous programming is implemented, let’s review how Generator functions are executed

// Declare a Generator function
function* gen(x){
	let y = yield x + 2
  return y
}
// Traversal object
let g = gen()
// Call the next method the first time
g.next()  // { value: 3, done: false }
// The second call passes arguments
g.next(2) // { value: 2, done: true }
Copy the code

The gen function is first executed to obtain the traverser object, which is not executed. When the next method of the traverser object is called, the first yield statement is executed, and so on

That is, only if the next method is called will the execution proceed

Meanwhile, in the above code, we can get the returned value by value and exchange data by passing parameters to the next method

Error handling mechanism

Error handling code can be deployed inside the Generator functions to catch errors thrown outside the function

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw('Wrong');
Copy the code

Maybe some people don’t understand why an internal catch can catch an external error?

The reason is that we throw the error through g.row, which is actually throwing the error into the generator, since we are calling the throw method on p after all

Implementing asynchronous programming

In my last article, I covered the execution mechanism of generators in detail, along with the yield execution characteristics, which you can read first

The idea is to use generator functions to implement asynchronous programming by using yield to suspend the execution of generator functions. Let’s look at an example

Generator + Promise
function * main () {
    const user = yield ajax('/api/usrs.json')
    console.log(user)
}
const g = main()
const result = g.next()
result.value.then(data= > {
    g.next(data)
})
Copy the code

First we define a generator function main, and then use yield inside that function to return an Ajax call, which returns a Promise object.

It then receives the return value of the yield statement, which is the argument to the second next method.

We can call the generator function from the outside to get its iterator object, and then call the next method on that object, so that the main function will execute to the first yield, which is the ajax call, In this case, the value of the object returned by the next method is the Promise object returned by Ajax

So we can specify the Promise callback with the then method, and in the Promise callback we can get the Promise execution result data, and then we can call the next method again, We pass our data so that main can continue, and data is assigned to the user as the yield expression return value

Asynchronous iterator

If generator + Promise above is anything to go by, this is simply asynchronous programming using a generator

function foo(x, y) {
    ajax("1.2.34.2".function(err,data) {
        if(err) {
            it.throw(err)
        }else {
            it.next(data)
        }
    })
}
function *main() {
    let text = yield foo(11.31)
	console.log( text )
}
const it = main()
it.next()
Copy the code

This is a simple example in the code above, and while it may seem like a lot more than the callback function implementation, you can see that the code logic is much better

The most critical piece of code in this

let text = yield foo(11.31)
console.log( text )
Copy the code

We explained this in the last part

In yield Foo (11, 31), foo(11, 31) is first called with no return value, a request is sent for data, the request succeeds, and it.next(data) is called, thus making data the return value of the previous yield, thus synchronizing the asynchronous code

async await

There is a lot more to Generator, tools, concurrency, delegates, and so on, which make generators very powerful, but also makes writing an executor function more cumbersome, so in ES7 we added async await keyword pairs, which are much easier to use.

Async functions are syntactic sugar for generator functions.

It is syntactically very similar to Generator functions, simply change Generator functions to async keyword modified functions and change yield to await. This function can be called directly from the outside and executed in exactly the same way as the Generator function

Compared with Generator async, the biggest advantage is that it does not need to be used with some tools, such as Co and Runner

The reason is that it is standard asynchronous programming at the language level, and async functions can return a Promise object, which also helps control the code.

Note that await can only appear in the async function body

// Change generator functions to async modified functions
async function main() {
    try {
        // change yield to await
        const a = await ajax('xxxx')
        console.log(a)
        const b = await ajax('xxx')
        console.log(b)
        const c = await ajax('xx')
        console.log(c)
    } catch (e) {
        console.log(e)
    }
}
// Return a Promise object
const promise = main()
Copy the code

As we can see from the code above, we do not need to control execution through next as Generator does

Async await is a combination of Generator and Promise that solves the problem left by the previous method and should be the best solution for handling asynchrony at the moment

conclusion

This article describes the four stages of asynchronous programming as a progressive process that addresses the problems of the previous approach step by step.

  1. The callback function: causes two problems
    • Lack of orderliness: Callback hell, resulting in code that is difficult to maintain, poorly read, and so on
    • Lack of trusted capricious: Inversion of control, resulting in code that may execute incorrectly
  2. Promise: Solved the problem of trusted caprices, but the code was too redundant
  3. Generator: Solves the sequential problem but requires manual controlnextAnd the code used with the tool can be very complex
  4. Async await: to combinegenerator + promise, no manual call, perfect solution

reference

  1. JavaScript asynchronous programming
  2. Asynchronous application of Generator functions
  3. JavaScript Advanced Programming (4th Edition)

That’s all for this article, hope you like 💛, any questions can be left in the comments section oh ~