preface

In the interview usually use the most things, once asked, but feel that they cannot explain clearly.

The essence of this is that sometimes we just learn how to use it without really understanding why the technology exists, what problems it solves, and how it evolves at every step.

Once we summarize, our answers to the interviewer’s questions will be straightforward.

Synchronous and asynchronous

The Javascript language is executed in a “single thread” environment.

Single-threaded means that you can only complete one task at a time. If there are multiple tasks, they must be queued, the first task completed, the next task executed, and so on.

The advantage of this mode is that the implementation is relatively simple, the execution environment is relatively simple; The disadvantage is that as long as one task takes a long time, subsequent tasks must wait in line, which will delay the execution of the entire program. A common browser non-response (suspended animation) is usually the result of a single piece of Javascript code running for so long (such as an infinite loop) that the entire page gets stuck in one place and no other task can be performed.

To solve this problem, the Javascript language divides the execution mode of the task into two modes: Synchronous and Asynchronous.

synchronous

“Synchronous mode” is the last task wait for the end of the previous task, and then execute, the execution sequence of the program is consistent with the order of the task, synchronous;

asynchronous

“Asynchronous mode” is completely different, each task has one or more of the callback function (the callback), before the end of a task, not to perform a task, after but the callback function, after a task is before the end of a task before execution, so the program execution order the order is not consistent with the task, asynchronous.

“Asynchronous mode” is very important. On the browser side, long operations should be performed asynchronously to prevent the browser from becoming unresponsive. The best example of this is Ajax operations.

The evolution of asynchronous JavaScript patterns

  1. callback
  2. promise
  3. Generator
  4. async

This is the history of asynchrony. Each approach has its advantages and disadvantages, and it is the inability to tolerate these disadvantages that drives the continuous iteration of technology.

Callback

SetTimeout (function(){console.log(" I am a callback function "); }, 300); $.ajax(url,function(){console.log(" I am a callback function "); });Copy the code

This was originally the way JavaScript handled asynchrony, and as the project became more difficult, especially as it sent requests in the background, it became more and more complex, easily leading to Callback Hell

$.ajax(url,function(){console.log(" I am the callback function 1"); $.ajax(url,function(){console.log(" I am a callback function 2"); $.ajax(url,function(){console.log(" I am the callback function 3"); }); }); });Copy the code

Back in the era of jQuery, many students have written similar code. Over time, requirements were suddenly added or changed, and we had to change the code layer by layer.

At the same time, both jquery and the community have started to come up with ideas of their own. For example, jquery introduced deferred objects, which were later incorporated into the ES6 specification and are now promises.

Promise

Promise is a solution to asynchronous programming that makes more sense and is more powerful than traditional solutions — callback functions and events. It was first proposed and implemented by the community, and ES6 has written it into the language standard, unifying usage, and providing Promise objects natively.

A Promise is simply a container that holds the result of an event (usually an asynchronous operation) that will end in the future. Syntactically, a Promise is an object from which to get messages for asynchronous operations. Promise provides a uniform API, and all kinds of asynchronous operations can be handled in the same way.

Use promise to encapsulate Ajax

const ajax = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
  });

  return promise;
};
Copy the code

Single layer use:

ajax("/posts.json").then(function(json) { console.log(json); }, function(error) {console.error(' error ', error); });Copy the code

Nested use:

ajax("/posts1.json").then(function(data1) { return ajax("/data1.url"); }).then(function(data2) { return ajax("/data2.url"); }).then(function(data3) { return ajax("/data3.url"); }).then(function(data4) { return ajax("/data4.url"); }).catch(function(error) {// Errors of the Promise object are "bubbling" and will be passed back until they are caught. That is, an error is always caught by the next catch statement. Console. log(' Error! ', error); });Copy the code

No matter how many layers of requests are nested, the structure is so neat that there is no callback hell anymore.

The concept of promises is fairly simple to understand, and since this article focuses on the evolution of asynchronous programming, we won’t go into the basics of Promises in detail.

Generator

Generator functions are an asynchronous programming solution provided by ES6 with completely different syntactic behavior from traditional functions.

Generator functions can be understood in many ways. Syntactically, the Generator function is a state machine that encapsulates multiple internal states.

Execution of Generator returns an traverser object, that is, Generator is also a traverser object Generator in addition to the state machine. A traverser object that iterates through each state within the Generator in turn.

Formally, a Generator function is an ordinary function, but has two characteristics.

  1. There is an asterisk between the function keyword and the function name;
  2. Inside the function body, use yield expressions to define different internal states (yield means “output” in English)
function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } // After a Generator is called, the function does not execute and returns not the result of the function's execution, but an Iterator Object pointing to the internal state. var hw = helloWorldGenerator(); hw.next(); // { value: 'hello', done: false } hw.next(); // { value: 'world', done: false } hw.next(); // { value: 'ending', done: true } hw.next(); // { value: undefined, done: true }Copy the code

Executing Generator returns an traverser object. Let’s look at how this traverser object works

class HelloWorldGenerator {

  constructor(arrVal) {
    this.array = arrVal;
    this.nextIndex = 0;
  }
  
  [Symbol.iterator]() { return this; }

  next() {
    return this.nextIndex < this.array.length ?
        {value: this.array[this.nextIndex++], done: false} :
        {value: undefined, done: true};
  }
  
}

function helloWorldGenerator() {
  return new HelloWorldGenerator(['hello','world','ending']);
}

var hw = helloWorldGenerator();
hw.next(); // { value: 'hello', done: false }
hw.next(); // { value: 'world', done: false }
hw.next(); // { value: 'ending', done: true }
hw.next(); // { value: undefined, done: true }
Copy the code

For each yield value in the helloWorldGenerator function, an array is added. Access the array in turn by calling the next() method to change the value of the pointer nextIndex.

Rules for yield expressions

Because the Generator returns a traverser object that only calls to the next method iterate over the next internal state, it actually provides a function to pause execution. The yield expression is the pause flag.

The next method of the traverser object runs as follows.

(1) When a yield expression is encountered, the following operation is paused, and the value immediately following the yield expression is used as the value of the returned object’s value property.

(2) The next time the next method is called, the execution continues until the next yield expression is encountered.

(3) If no new yield expression is encountered, the function is run until the end of the return statement, and the value of the expression following the return statement is used as the value of the returned object’s value property.

(4) If the function does not have a return statement, the value attribute of the returned object is undefined.

Yield expressions can only be used in Generator functions and will yield an error if used elsewhere.

Next method

Parameters to the next method

The yield expression itself returns no value, or always returns undefined. The next method can take an argument that is treated as the return value of the previous yield expression.

function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5); A.ext () // Object{value:6, done:false} So y = undefined a.ext () // Object{value:NaN, done:false} done:true} var b = foo(5); B.ext () // {value:6, done:false} b.ext (12) // {value:8, done:false} 12 is used for the last yield, so y=12*2, Value = 8 b.ext (13) // {value:42, done:true} z = 13, value = 13 + 24 + 5 = 42Copy the code

This feature has important syntactic implications. The context state of a Generator function does not change from its paused state to its resumed state. With the parameters of the next method, there is a way to continue injecting values into the function body after the Generator function has started running. That is, the behavior of the Generator function can be adjusted by injecting different values from outside to inside at different stages of its operation.

Note that because the argument to the next method represents the return value of the previous yield expression, passing the argument is invalid the first time the next method is used. The V8 engine ignores arguments from the first use of the next method, which are valid only from the second use of the next method. Semantically, the first next method is used to start the traverser object, so it doesn’t take arguments.

coroutines

Traditional programming languages have long had solutions for asynchronous programming (actually multitasking). One of these is called a coroutine, which means that multiple threads work together 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 one, coroutine A starts executing.
  • In the second step, coroutine A is paused halfway through execution, and execution authority is transferred to coroutine B.
  • In the third step, the coroutine B returns execution authority.
  • Step 4, coroutine A resumes execution.

The coroutine A of the above process is an asynchronous task because it is executed in two (or more) segments.

function* asyncJob() { // ... Var f = yield readFile(fileA); / /... Other code}Copy the code

The asyncJob function in the above code is a coroutine whose secret is the yield command. It says execution at this point, execution will be handed over to some other coroutine. That is, the yield command is the boundary between the two phases of asynchrony.

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

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/users/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 is very much 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, you first execute the Generator function to retrieve the traverser object, and 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 is called using the then method.

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.

A Generator is a container for asynchronous operations. Its automatic execution requires a mechanism that automatically cedes execution rights when an asynchronous operation has a result. There are two ways to do this.

(1) Callback function. Wrap the asynchronous operation as a Thunk function, handing back execution authority in the callback function.

(2) Promise objects. Wrap the asynchronous operation as a Promise object and hand back execution with the then method.

Encapsulate automatic execution based on Promise objects

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
Copy the code

Then, execute the Generator function above manually.

var g = gen();

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

Manual execution is simply adding layers of callbacks using the THEN method. With this in mind, you can write an autoexecutor.

function run(gen){ var g = gen(); function next(data){ var result = g.next(data); if (result.done) return result.value; Result.value. then(function(data){next(data); function(data){next(data); }); } next(); } run(gen);Copy the code

At the end of the day, Generator functions simply provide synchronous writing of asynchronous functions and do not have asynchronous processing capabilities. Promises essentially wrap callback functions to handle asynchrony.

Co module

The CO module is a small tool released in June 2013 by TJ Holowaychuk, a well-known programmer, for automatic execution of Generator functions.

var gen = function* () {
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
Copy the code

The CO module saves you from writing actuators for Generator functions.

var co = require('co');
co(gen);
Copy the code

async + await

It is the syntactic sugar of the Generator function

Let’s change the above case to async + await

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
Copy the code

A comparison shows that async simply replaces the asterisk (*) of Generator functions with async and yields with await. But not only that, async improves on the yield function in four other ways.

1. Built-in actuator

Generator functions must be executed by an executor, hence the CO module, while async functions have their own executor. In other words, async functions are executed exactly like normal functions, with only one line.

2. 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.

3. Wider applicability

According to the CO module convention, yield can only be followed by Thunk or Promise, while async can be followed by await and Promise and primitive type values (numeric, string, Boolean, etc.). But that automatically changes to an immediate Resolved Promise).

Async functions return a Promise object, which is much more convenient than Generator functions that return 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 function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});
Copy the code

Async await note

1, await command placed in try… In the catch block

The reason is that any Promise object after an await statement goes reject and the whole async function breaks.

async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } try { await otherthingThatReturnsAPromise(); } catch (err) { console.log(err); Another writing}} / / async function myFunction () {await somethingThatReturnsAPromise (). The catch (function (err) { console.log(err); }); await otherthingThatReturnsAPromise() .catch(function (err) { console.log(err); }); }Copy the code

2, asynchronous operations after multiple await commands, if there is no secondary relationship, it is better to let them fire at the same time.

// let [foo, bar] = await Promise. All ([getFoo(), getBar()]); // let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise;Copy the code

3, await command can only be used in async functions. If used in normal functions, an error will be reported

async function dbFuc(db) { let docs = [{}, {}, {}]; ForEach (function (doc) {await db.post(doc); }); }Copy the code

4. Async functions can preserve the run stack

const a = () => {
  b().then(() => c());
};
Copy the code

In the above code, function A runs an asynchronous task b() inside. When b() runs, function A () does not interrupt, but continues execution. By the time b() finishes running, it is possible that A () has long since finished running, and the context in which B () exists has disappeared. If b() or c() reports an error, the error stack will not include a().

Now change this example to an async function.

const a = async () => {
  await b();
  c();
};
Copy the code

In the code above, when b() runs, a() is paused, and the context is saved. If b() or c() reports an error, the error stack will include a(). This is a feature of generator functions.

summary

This article is basically a reference to ECMAScript 6 Introduction. The purpose of this resummary is to use a more concise language that leaves out much of the usage detail to explain the step-by-step evolution of asynchronous programming, as well as the most important and difficult to understand Generator functions.