start

I think we are all familiar with the concept of synchronization and asynchrony. Since asynchrony is an unshakeable part of our programming, let’s take a look at the history of asynchrony today. Callback hell -> promise -> generator -> generator + thunk -> async/await

callback hell

Without further ado, let’s start with the code

fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    fs.readFile(fileC, function (err, data) {
      // ...
    });
  });
});
Copy the code

It’s not hard to imagine multiple nesting if multiple files are read in sequence. Code doesn’t grow vertically, it grows horizontally, and if you change an operation in the middle, it will affect its “front and back neighbors” and quickly become an unmanageable mess. Time makes a hero, and that’s when Promise came in.

promise

It is not a new syntactic feature, but a new way of writing that allows you to change the nesting of callback functions to chain calls. Use Promise to read multiple files in succession, written as follows.

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});
Copy the code

Then returns a Promise object. As you can see, the callbacks are rendered in chained ways, making maintenance easier. In plain English, promises are just a weakness of a better programming style: The “THEN” and “catch” are confusing, and the semantics of the operations themselves are hard to see

The generator function

Let’s get rid of the concept of a “coroutine,” which, as the name suggests, is a task that multiple threads work together to complete

Take a chestnut

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

Yiled will pause the program and next will start execution. Calling a Generator function returns an internal pointer (the traverser) g. This is another way in which a Generator function differs from a normal 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, as in x + 2.

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 properties). 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 another phase.

The practical application of gennerator function for asynchronous tasks

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}
var g = gen();
var result = g.next();

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

Disadvantages:

  1. As you can see, while Generator functions represent asynchronous operations succinctly, process management (i.e. when to perform phase one and when to perform phase two) is not convenient.
  2. You need to manually call the iterator’s next method to continue execution

Automatic process management for the generator

An autoexecutor based on the Thunk function,

The thunk function of the generator is a function that encapsulates a multi-argument function into a single argument function and applies the remaining arguments to the callback function

Continue execution of the generator function, since the yield command removes execution authority from the program. For example, the next method in this example is a callback to the thunk function

function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

function* g() {
  // ...
}

run(g);
Copy the code

With this actuator, it is much easier to execute Generator functions

Take an example of a real asynchronous request, an automatic actuator in action. Some of the later mature CO modules, Redux-Saga should be the essence of this idea to achieve their own asynchronous implementation

Iterator concept

Generator functions need to be iterated by iterators, so let me give you a brief introduction. Iterators are objects with special interfaces. With a next() method, the call returns an object containing two properties, value and done. Value indicates the value of the current position, done indicates whether the iteration is complete, and when true, the call to next is invalid. In ES5, traversing collections is usually a for loop, arrays and forEach methods, and objects are for-in. In ES6, Map and Set are added, and iterators can uniformly handle all collection data methods. An iterator is an interface, and as long as you have a data structure that exposes an iterator’s interface, you can iterate. ES6 created a new traversal command for… The of loop is an Iterator interface for… Of consumption. Data structures are considered “Iterable” whenever the Iterator interface is deployed. ES6 states that the default Iterator interface is deployed in the symbol. Iterator property of a data structure, or that a data structure can be considered “iterable” as long as it has symbol. Iterator data.

Can be used for… Native data structures for consumption of

  • Array
  • Map
  • Set
  • String
  • TypedArray (a generic fixed-length buffer type that allows binary data to be read from the buffer)
  • Arguments object in the function
  • The NodeList object

With the iterator interface, data structures can not only be used for… The of loop iterates, or you can use the while loop.

var $iterator = ITERABLE[Symbol.iterator](); var $result = $iterator.next(); while (! $result.done) { var x = $result.value; / /... $result = $iterator.next(); }Copy the code

In the above code, ITERABLE represents some traversable data structure, and $iterator is its iterator object. Each time the iterator moves the pointer (next method), the done property of the returned value is checked. If the iteration is not complete, the iterator moves the pointer to the next step (next method), repeating the loop.

Compare the three types of iteration and see why the third party library implements its autoexecutor recursively:

  • for … Of can only call iterator’s.next methods, not.return and.throw, which are not controlled and cannot handle asynchracy.

  • The while loop can call.next-return.throw, which is completely controlled, but cannot handle asynchrony.

  • Recursion: Fully controlled and able to handle asynchrony.

Async functions (recommended)

New in ES7, it is an improvement on the Generator function, async function has its own executor. Its return value is a Promise object, which is much more convenient than Generator’s return value being an Iterator. You can specify what to do next using the then method.

Async functions can be thought of as multiple asynchronous operations wrapped as a Promise object, and await commands are syntactic sugar for internal THEN commands.

Async functions are implemented by wrapping genrator and autoexecutor in one function

Comparison of Promise, Generator, async functions

Use an animation example from Ruan Yifeng to illustrate the three

Suppose a series of animations are deployed on a DOM element, and the first animation ends before the next one begins. If one of the animations fails, it does not proceed further and returns the return value of the last successfully executed animation.

Written Promise
Function chainAnimationsPromise(elem, animations) {// The variable ret is used to keep the return value of the previous animation let ret = null; // create an empty Promise let p = promise.resolve (); For (let anim of animations) {p = p. Chen (function(val) {ret = val; return anim(elem); }); Function (e) {/* ignore the error, continue */}). Then (function() {return ret; }); }Copy the code

While the Promise writing is a big improvement over the callback writing, at first glance the code is all about the Promise API (then, catch, and so on), and the semantics of the actions themselves are not easy to see.

Writing of a Generator function
function chainAnimationsGenerator(elem, animations) { return spawn(function*() { let ret = null; try { for(let anim of animations) { ret = yield anim(elem); }} catch(e) {return ret; }); }Copy the code

The above code iterates through each animation using Generator functions, with clearer semantics than Promise, and all user-defined actions appear inside the spawn function. The problem with this is that you must have a task runner that automatically executes the Generator. The spawn function of the above code is the spawn function that returns a Promise object, and the expression following the yield statement must return a Promise

Async function writing method
async function chainAnimationsAsync(elem, animations) { let ret = null; try { for(let anim of animations) { ret = await anim(elem); }} catch(e) {return ret; }Copy the code

It can be seen that the implementation of Async functions is the simplest and most semantic, with almost no semantically irrelevant code. It provides the automatic executor in Generator writing at the language level without exposing it to the user, thus minimizing the amount of code. If written with Generator, the autoexecutor needs to be supplied by the user.

The goal of asynchronous programming is to make asynchronous logic code look synchronous; To Async/await; I think it’s a milestone in the quest to deal with asynchronous programming