Writing in the front

Here’s an interesting question:

Why must the first argument to the Node.js convention callback be the error object err(or null if there is no error)?

The reason is that the callback is executed asynchronously, and its execution is divided into two segments. The program cannot catch the errors thrown between these two segments, so it can only be passed as arguments to the second segment. As you know, JavaScript has only one thread, and without asynchronous editing, complex programs are almost impossible to use. Before ES6, there were basically four ways to program asynchronously:

  • The callback function

  • Event listeners

  • Publish/subscribe

  • Promise object

ES6 brings JavaScript asynchronous programming to a new stage, and the async function in ES7 is the ultimate solution to asynchronous programming. The following will specifically explain the principles of asynchronous programming and noteworthy places, I wait to elaborate ~

Evolution of asynchronous programming

A basic understanding

Asynchronism simply means that a task is divided into two parts, the first part is performed, then the other part is performed, and then the second part is performed when you are ready.

For example, to read a file for processing, the first part of the task is to send a request to the operating system to read the file. The program then performs other tasks, waits until the operating system returns the file, and then proceeds to the second part of the task (processing the file). This discontinuous execution is called asynchrony.

Accordingly, continuous execution is called synchronization. Because the execution is continuous, no other tasks can be inserted, so the program has to wait while the operating system reads the file from the hard disk.

The callback function

The so-called callback function is to write the second section of the task in a separate function, which is called when the task is re-executed. Its English name callback literally means “callback”.

In the example above, a file read operation looks like this:

fs.readFile(fileA, (err, data) => {
    if (err) throw err;
    console.log(data)
})

fs.readFile(fileB, (err, data) => {
    if (err) throw err;
    console.log(data)
})Copy the code

Note that the above two pieces of code are asynchronous to each other, and while they start execution from top to bottom, the second section does not wait until the end of the first section, but executes concurrently.

If you want to wait for fileB to start executing after fileA is successfully read, what should you do? The simplest way to do this is through callback nesting:

fs.readFile(fileA, (err, data) => {
    if (err) throw err;
    console.log(data)
    
    fs.readFile(fileB, (_err, _data) => { 
        if (_err) throw err;
        console.log(_data)
    })
})Copy the code

This way I can only tolerate the nesting of one digit, and it makes the code horizontal development, is really an ugly stroke, many times it is impossible to see. What if you had to perform 100 asynchronous operations synchronously? Go crazy! Is there a better way?

usePromise

To be clear, the Promise concept is not new to ES6, but rather integrated into a new way of writing. Continuing with the previous example, using the Promise code looks like this:

var readFile = require('fs-readfile-promise'); readFile(fileA) .then((data)=>{console.log(data)}) .then(()=>{return readFile(fileB)}) .then((data)=>{console.log(data)}) // ... Read n times. Catch ((err)=>{console.log(err)})Copy the code

Note: this code uses Node’s version of the Promise wrapped readFile function, which returns a Promise object.

var fs = require('fs');

var readFile = function(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if (err) reject(err)
            resolve(data)
        })
    })
}

module.export = readFileCopy the code

However, the way Promise is written is just an improvement on the callback function, and with then(), the two pieces of asynchronous task execution are more visible, but nothing new. Regardless of their advantages, the biggest problem with promises is that the original tasks are wrapped up in promises, so that no matter what the action is, it looks like a bunch of THEN () at first glance, and the original meaning becomes unclear.

Ask god, does MD have a better way?

useGenerator

Before introducing generator, what is a coroutine

“Ctrip in hand, say leave”. Haha, don’t be confused. Coroutine is not Ctrip.

coroutines

A “coroutine” is when multiple threads collaborate to complete asynchronous tasks. Coroutines are a little bit like functions and a little bit like threads. Its operation process is roughly as follows:

  • Step 1: Coroutine A begins execution

  • Step 2: The execution of coroutine A is suspended halfway, and the execution authority is transferred to coroutine B

  • Step 3: After a period of time, coroutine B returns execution

  • Step 4: Coroutine A resumes execution

function asyncJob() {
    // ... 其他代码
    var f = yield readFile(fileA);
    // ... 其他代码 
}Copy the code

The asyncJob() above is a coroutine whose secret is the yield command. It means that execution at this point is handed over to another coroutine. In other words, yield is the boundary between two asynchronous phases.

The coroutine pauses at yield, returns to execution, and continues from where it was suspended. The biggest advantage is that the code is written very much like a synchronous operation, which is exactly the same except for the yield command.

Generatorfunction

Generator functions are implementations of coroutines in ES6, with the best feature of surrendering execution of functions (i.e., suspending execution). The whole Generator function is a encapsulated asynchronous task, or a container for asynchronous tasks.

function* gen(x) {
    var y = yield x + 2;
    return y;
} 

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

In the above code, calling a Generator function returns an internal pointer (the traverser) g. This is another way in which a Generator function differs from a regular function in that it does not return a result, but rather a pointer object. Calling the next() method on pointer G moves the internal pointer (that is, the first segment of an asynchronous task) to the first yield statement encountered.

In other words, the next() method performs the Generator function in stages. Each time the next() method is called, an object is returned representing information about the current stage (value and done attributes). The value attribute is the value of the expression following the yield statement and represents the value of the current phase; The done attribute is a Boolean value indicating whether the Generator has completed execution, that is, whether there is one more phase.

GeneratorFunction data exchange and error handling

The Generator function can suspend and resume execution, which is the fundamental reason it encapsulates asynchronous tasks. In addition, it has two features that make it a solution for asynchronous programming: data exchange and error handling inside and outside functions.

The value property of the value returned by the next() method is the data output by the Generator function; The next() method can also take arguments and input data into the body of the Generator function.

function* gen(x) {
    var y = yield x + 2;
    return y;
} 

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

In the code above, the value property of the first next() method returns the value (3) of the expression x+2. The second next() method takes 2, which can be passed to the Generator function as the result of the asynchronous task in the previous stage and is received by the variable Y in the function body, so the value attribute in this step returns 2 (the value of variable Y).

Error handling code can also 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.row (' gone wrong ');Copy the code

The last line of the code above, outside of the Generator, throws an error using a pointer object’s throw method that can be thrown by a try… Catch code block capture. This means that there is a separation in time and space between the code that makes mistakes and the code that handles them, which is important for asynchronous programming.

Encapsulation of asynchronous tasks

Let’s see how to use the Generator function to perform a real asynchronous task.

var fetch = require('node-fetch')

function* gen() {
    var url = 'https://api.github.com/usrs/github';
    var result = yield fetch(url);
    console.log(result.bio);
} Copy the code

In the code above, the Generator function encapsulates an asynchronous operation that reads a remote interface and then parses the information from jSON-formatted data. As mentioned earlier, this code looks a lot like a synchronous operation. Except for the yield command.

This code is executed as follows:

var g = gen();
var result = g.next();

result.value.then(function(data) {
    return data.json()
}).then(function(data) {
    g.next(data)
});Copy the code

In the above code, we first execute the Generator function to get the traverser object. Then use the next() method to perform the first phase of the asynchronous task. Since the Fetch module returns a Promise object, the next next() method needs to be called with the then() method.

As you can see, while Generator functions represent asynchronous operations succinct, process management is not convenient (i.e., when phase ONE is appropriate and phase two is appropriate)

Enter the big Bossasyncfunction

Async functions are syntactic sugar for Generator functions.

Continuing with our asynchronous file reading example, use Generator implementation

var fs = require('fs');

var readFile = (path) => {
    return new Promise((resolve, reject) => {
        fs.readFile(path, (err, data) => {
            if (err) reject(err)
            resolve(data)
        })
    })
}

var gen = function* () {
    var f1 = yield readFile(fileA);
    var f2 = yield readFile(fileB);
    console.log(f1.toString());
    console.log(f2.toString());
}Copy the code

Write it as async, which looks like this:

var asyncReadFile = async function() {
    var f1 = await readFile(fileA);
    var f2 = await readFile(fileB);
    console.log(f1.toString())
    console.log(f2.toString())
}Copy the code

The async function replaces the * of the Generator with async and yields with await. In addition, there are four improvements to the Generator:

(1) Built-in actuators. The execution of Generator functions relies on actuators, hence the existence of asynchronous actuators such as the CO module, while async functions come with their own actuators. In other words, async functions are executed exactly like normal functions with one line:

var result = asyncReadFile();Copy the code

(2) The above code calls asyncReadFile(), which will be executed automatically and output the final result. This is not at all like a Generator function, where you need to call the next() method, or use the CO module, to actually execute and get the final result.

(3) Better semantics. Async and await are semantic clearer than asterisks and yield. Async means that there is an asynchronous operation in a function, and await means that the following expression needs to wait for the result.

(4) Wider applicability. The await command of an async function can be followed by a Promise object and a value of primitive type (numeric, string, and Boolean, which is equivalent to a synchronous operation).

(5) Return a Promise, which is much more convenient than a Generator that returns an Iterator. You can specify the next action using then().

Further, async functions can be thought of as a single Promise object wrapped in multiple asynchronous operations, and await commands are syntactic sugar for internal THEN () commands.

Realize the principle of

An async function is implemented by wrapping a Generator and an autoexecutor in a single function. The following code:

async function fn(args) { // ... Function fn(args) {return spawn(function*() {//... Spawn (genF) {return new Promise(resolve, reject) {var gen = genF(); function step(nextF) { try { var next = nextF() } catch(e) { return reject(e) } if (next.done) { return resolve(next.value) } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v) }) },function(e) { step(function() { return gen.throw(e) }) }) } step(function() { return gen.next(undefined) }) }) }Copy the code

asyncThe usage function

(1) Async returns a Promise object, and then() adds a callback. (2) When a function executes, it will return once it encounters await(), wait until the triggered asynchronous operation is complete, and then execute the following statement in the function body.

Here is an example of delayed output:

function timeout(ms) { return new Promise((resolve) => { setTimeout(resolve, ms) }) } async function asyncPrint(value, Ms) {await timeout(ms) console.log(value)} asyncPrint('Hello World! ', 500).Copy the code

Matters needing attention

(1) Await a Promise object, which may result in a reject, so it is better to await it in a try… In the catch block.

(2) await command can only be used in async functions, normal functions will report an error.

(3) ES6 adds await to reserved word. Using this word as an identifier is legal in ES5, but ES6 throws a SyntaxError.

The ultimate war

Let’s take a look at an example of async versus Promise and Generator.

Requirement: Assume that a series of animations are deployed on a DOM element, and the first animation ends before the next one begins. If another animation fails, no further animation is executed and the return value of the last successful animation is returned.

withPromiseimplementation

Function chainAnimationsPromise(ele, animations) {var ret = null; Var p = promise.resolve (); For (var anim in animations) {p = p. Chen (function(val) {ret = val; return anim(ele); Function (e) {function(e) {function(e) {return ret; })}Copy the code

While the way Promise is written is a big improvement over the way callback functions are written, the semantics of the operations themselves are less clear.

withGeneratorimplementation

function chainAnimationsGenerator(ele, animations) { return spawn(function*() { var ret = null; Try {for(var anim of animations) {ret = yield anim(ele)}} catch(e) {/* Ignore error, continue */} return ret; })}Copy the code

Using Generator is semantically cleaner than Promise, but all user-defined actions occur inside the spawn function. The problem with this formulation is that a task runner must automatically execute a Generator that returns a Promise object and guarantees that the expression after the yield statement returns a Promise. Spawn, above, played that role. Its implementation is as follows:

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    var gen = genF();
    function step(nextF) {
      try {
        var next = nextF()
      } catch(e) {
        return reject(e)
      }
      if (next.done) {
        return resolve(next.value)
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v) })
      },function(e) {
        step(function() { return gen.throw(e) })
      })
    }
    step(function() { return gen.next(undefined) })
  })
}Copy the code

useasyncimplementation

async function chainAnimationAsync(ele, animations) { var ret = null; Try {for(var anim of animations) {ret = await anim(ele)}} catch(e) {/* Ignore error, continue */} return ret; }Copy the code

Ok, light from the amount of code to see the advantage! Concise and semantic, there is almost no irrelevant code. Victory!

Note: Async is a proposal of ES7. Use Babel or ReGenerator for transcoding.

reference

Ruan Yifeng, Introduction to ES6 Standards

Welcome to my Github and personal blog -Jafeney