This is the first day of my participation in the August More Text challenge

Async function

meaning

The ES2017 standard introduces async functions to make asynchronous operations more convenient.

What is async function? In short, it is the syntactic sugar of Generator functions.

There is a Generator function that reads two files in turn.

const fs = require('fs');

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

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

The function gen in the above code can be written as async, as follows.

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 an async function simply replaces the asterisk (*) of a Generator function with async, yields with await, and nothing more.

The improvements of async over Generator are shown in the following four aspects.

(1) Built-in actuators.

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.

asyncReadFile();
Copy the code

The above code calls the asyncReadFile function, which then automatically executes and outputs 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.

(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).

(4) Return the Promise.

Async functions return a Promise object, which is much more convenient than Generator functions returning an Iterator. You can specify what to do next using the then method.

Further, 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.

Basic usage

The async function returns a Promise object, and callbacks can be added using the then method. When a function executes, it returns as soon as it encounters await, waits for the asynchronous operation to complete, and then executes the following statement in the function body.

Here’s an example.

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

The above code is a function to get a stock quote. The async keyword in front of the function indicates that there is an asynchronous operation inside the function. When this function is called, a Promise object is immediately returned.

Here is another example that specifies how many milliseconds to output a value.

function timeout(ms) {
  return new Promise((resolve) = > {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world'.50);
Copy the code

The code above specifies that after 50 milliseconds, it prints Hello World.

Since async functions return a Promise object, they can be used as an argument to an await command. Therefore, the above example can also be written in the following form.

async function timeout(ms) {
  await new Promise((resolve) = > {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world'.50);
Copy the code

Async functions can be used in many ways.

// Function declaration
async function foo() {}

// Function expression
const foo = async function () {};

// Object method
let obj = { async foo(){}}; obj.foo().then(...)// Class method
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`); }}const storage = new Storage();
storage.getAvatar('jake'). Then (...). ;// Arrow function
const foo = async() = > {};Copy the code

grammar

The syntactic rules of async function are relatively simple in general, but the difficulty is the error handling mechanism.

Return a Promise object

The async function returns a Promise object.

The value returned by the return statement inside the async function becomes an argument to the then method callback function.

async function f() {
  return 'hello world';
}

f().then(v= > console.log(v))
// "hello world"
Copy the code

In the above code, the value returned by the return command inside f is received by the then callback.

An error is thrown inside the async function, which causes the returned Promise object to become reject. The thrown error object is received by the catch method callback.

async function f() {
  throw new Error('Wrong');
}

f().then(
  v= > console.log('resolve', v),
  e= > console.log('reject', e)
)
//reject Error: Reject Error
Copy the code

The state change of the Promise object

Promise objects returned by async functions will not change state until all Promise objects following the internal await command have been executed, unless a return statement or an error is thrown. That is, the callback function specified by the THEN method is executed only after the asynchronous operation inside the async function is completed.

Here’s an example.

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i) [1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
Copy the code

In the above code, the function getTitle has three internal operations: fetch the page, retrieve the text, and match the page title. The console.log in the then method is executed only when all three operations are complete.

Await orders

Normally, the await command is followed by a Promise object that returns the result of that object. If it is not a Promise object, the corresponding value is returned.

async function f() {
  / / is equivalent to
  // return 123;
  return await 123;
}

f().then(v= > console.log(v))
/ / 123
Copy the code

In the above code, the argument to the await command is 123, which is equivalent to return 123.

Alternatively, if an await command is followed by a Thenable object (that is, the object that defines the then method), await will equate it with a Promise object.

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(
      () = > resolve(Date.now() - startTime),
      this.timeout ); }} (async() = > {const sleepTime = await new Sleep(1000);
  console.log(sleepTime); }) ();/ / 1000
Copy the code

In the code above, the await command is followed by an instance of the Sleep object. This instance is not a Promise object, but because the then method is defined, await will treat it as a Promise.

This example also demonstrates how to implement hibernation effect. JavaScript has no dormant syntax, but with the await command you can pause a program for a specified time. A simplified implementation of Sleep is given below.

function sleep(interval) {
  return new Promise(resolve= > {
    setTimeout(resolve, interval); })}/ / usage
async function one2FiveInAsync() {
  for(let i = 1; i <= 5; i++) {
    console.log(i);
    await sleep(1000);
  }
}

one2FiveInAsync();
Copy the code

If the Promise object following the await command becomes reject, the reject argument is received by the catch callback.

async function f() {
  await Promise.reject('Wrong');
}

f()
.then(v= > console.log(v))
.catch(e= > console.log(e))
/ / make a mistake
Copy the code

Notice that the await statement is preceded by no return, but the reject argument is still passed to the catch callback. Here the effect is the same if return is preceded by await.

Any Promise object after an await statement becomes reject, and the entire async function breaks.

async function f() {
  await Promise.reject('Wrong');
  await Promise.resolve('hello world'); // Will not be executed
}
Copy the code

In the above code, the second await statement will not be executed because the state of the first await statement changes to reject.

Sometimes we want to not interrupt subsequent asynchronous operations even if the previous one fails. We can then put the first await in a try… Inside the catch structure, so that the second await is executed regardless of whether the asynchronous operation succeeds or not.

async function f() {
  try {
    await Promise.reject('Wrong');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v= > console.log(v))
// hello world
Copy the code

The alternative is to await a Promise object followed by a catch method to handle any errors that may occur earlier.

async function f() {
  await Promise.reject('Wrong')
    .catch(e= > console.log(e));
  return await Promise.resolve('hello world');
}

f()
.then(v= > console.log(v))
/ / make a mistake
// hello world
Copy the code

Error handling

If an asynchronous operation following await fails, the Promise object returned by the async function is rejected.

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('Wrong');
  });
}

f()
.then(v= > console.log(v))
.catch(e= > console.log(e))
// Error: there is an Error
Copy the code

In the above code, after async f is executed, the Promise object behind await will throw an error object, causing the callback of the catch method to be called with the argument of the thrown error object. For specific execution mechanism, please refer to “Implementation Principle of Async Function” later.

The way to prevent errors is to put it in a try… Inside the catch block.

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('Wrong');
    });
  } catch(e) {
  }
  return await('hello world');
}
Copy the code

If you have more than one await command, you can put the try… Catch structure.

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err); }}Copy the code

The following example uses try… Catch structure to achieve multiple repeated attempts.

const superagent = require('superagent');
const NUM_RETRIES = 3;

async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      await superagent.get('http://google.com/this-throws-an-error');
      break;
    } catch(err) {}
  }
  console.log(i); / / 3
}

test();
Copy the code

In the above code, if the await operation succeeds, a break statement is used to exit the loop; If it fails, it is caught by a catch statement and the next loop is passed.

Use caution points

The Promise after await may result in rejected, so it is better to await await in a try… In the catch block.

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err); }}// Another way to write it

async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  });
}
Copy the code

Second, asynchronous operations followed by multiple await commands are best fired at the same time if there is no secondary relationship.

let foo = await getFoo();
let bar = await getBar();
Copy the code

In the code above, getFoo and getBar are two independent asynchronous (that is, non-dependent) operations written as secondary. This is time consuming because getBar will not be executed until getFoo is done, and they can be fired at the same time.

/ / write one
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

/ / write two
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
Copy the code

Both methods, getFoo and getBar, fire at the same time, thus shortening the execution time of the program.

Third, the 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 = [{}, {}, {}];

  / / an error
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}
Copy the code

The above code will report an error because await is used in normal functions. However, if you change the argument of forEach to async, there are problems.

function dbFuc(db) { // Async is not required here
  let docs = [{}, {}, {}];

  // May get an error result
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}
Copy the code

The above code may not work because the three db.post() operations will be executed concurrently, that is, at the same time, rather than secondarily. The correct way to write this is to use the for loop.

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    awaitdb.post(doc); }}Copy the code

Another approach is to use the reduce() method of arrays.

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  await docs.reduce(async (_, doc) => {
    await _;
    await db.post(doc);
  }, undefined);
}
Copy the code

In the example above, the first argument to the reduce() method is an async function, resulting in the first argument to the function being the Promise object returned by the previous operation, so you must await it with await. In addition, the reduce() method returns the result of the async function of the last member of the Docs array, which is also a Promise object, causing it to have to be preceded with await as well.

There is no return statement in the reduce() argument function above, because the main purpose of this function is db.post(), not return values. And async always returns a Promise object with or without a return, so a return is not necessary.

If you really want multiple requests to execute concurrently, you can use the promise.all method. When all three requests are resolved, these two options will have the same effect.

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) = > db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// Or use the following notation

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) = > db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}
Copy the code

Fourth, 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().

The implementation principle of async function

Async functions are implemented by wrapping Generator functions and automatic actuators in one function.

async function fn(args) {
  // ...
}

/ / is equivalent to

function fn(args) {
  return spawn(function* () {
    // ...
  });
}
Copy the code

All async functions can be written in the second form above, where the spawn function is the auto-executor.

Here’s an implementation of the spawn function, which is basically a copy of the autoexecutor above.

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        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

Comparison with other asynchronous processing methods

Let’s use an example to see how async functions compare with Promise and Generator functions.

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.

First, the way Promise is written.

function chainAnimationsPromise(elem, animations) {

  // The ret variable is used to hold the return value of the previous animation
  let ret = null;

  // Create an empty Promise
  let p = Promise.resolve();

  // Add all animations using the then method
  for(let anim of animations) {
    p = p.then(function(val) {
      ret = val;
      return anim(elem);
    });
  }

  // Return a Promise with an error-catching mechanism deployed
  return p.catch(function(e) {
    /* Ignore the error and continue with */
  }).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.

Next, write the Generator function.

function chainAnimationsGenerator(elem, animations) {

  return spawn(function* () {
    let ret = null;
    try {
      for(let anim of animations) {
        ret = yieldanim(elem); }}catch(e) {
      /* Ignore the error and continue with */
    }
    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.

Finally, write async function.

async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  try {
    for(let anim of animations) {
      ret = awaitanim(elem); }}catch(e) {
    /* Ignore the error and continue with */
  }
  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.

Example: Complete asynchronous operations sequentially

In real life development, you often encounter a set of asynchronous operations that need to be done sequentially. For example, read a set of urls remotely in turn and output the results in the order they were read.

Promise is written like this.

function logInOrder(urls) {
  // Read all urls remotely
  const textPromises = urls.map(url= > {
    return fetch(url).then(response= > response.text());
  });

  // Output in order
  textPromises.reduce((chain, textPromise) = > {
    return chain.then(() = > textPromise)
      .then(text= > console.log(text));
  }, Promise.resolve());
}
Copy the code

The code above uses the fetch method to read a set of urls remotely simultaneously. Each fetch operation returns a Promise object into the textPromises array. The Reduce method then processes each Promise object in turn, and then uses then to concatenate all the Promise objects so that the results can be output in turn.

It’s not very intuitive, it’s not very readable. Below is the async function implementation.

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(awaitresponse.text()); }}Copy the code

The above code is really much simpler, but the problem is that all remote operations are secondary. Only when the previous URL returns a result will the next URL be read, which is inefficient and a waste of time. What we need is to make remote requests concurrently.

async function logInOrder(urls) {
  // Read the remote URL concurrently
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // Output in order
  for (const textPromise of textPromises) {
    console.log(awaittextPromise); }}Copy the code

In the above code, although the map method takes an async function as an argument, it is executed concurrently, because only the async function is executed internally, and the outside is not affected. Behind the for… The of loop uses await inside, so it outputs sequentially.

The top await

According to the syntax specification, await commands can only appear inside async functions, otherwise an error will be reported.

/ / an error
const data = await fetch('https://api.example.com');
Copy the code

In the above code, an error will be reported if the await command is used independently and not placed inside the async function.

Currently, there is a syntactic proposal to allow independent use of await commands at the top level of the module so that the above line of code does not report errors. The purpose of this proposal is to use await to solve the problem of asynchronous loading of modules.

// awaiting.js
let output;
async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
}
main();
export { output };
Copy the code

In the code above, the output value of the module’s awaiting.js output depends on the asynchronous operation. We wrap the asynchronous operation in an async function and call this function. The variable output will have a value only after all the asynchronous operations have been executed, otherwise undefined will be returned.

The above code could also be written as an immediate function.

// awaiting.js
let output;
(async function main() {
  const dynamic = await import(someMission);
  const data = awaitfetch(url); output = someProcess(dynamic.default, data); }) ();export { output };
Copy the code

Here’s how to load the module.

// usage.js
import { output } from "./awaiting.js";

function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100));
setTimeout(() = > console.log(outputPlusValue(100)), 1000);
Copy the code

In the code above, the result of outputPlusValue() depends entirely on the execution time. The output value loaded in is undefined if the asynchronous operation in awaiting. Js is not finished executing.

The current solution is to have the original module output a Promise object from which to determine if the asynchronous operation has ended.

// awaiting.js
let output;
export default (async function main() {
  const dynamic = await import(someMission);
  const data = awaitfetch(url); output = someProcess(dynamic.default, data); }) ();export { output };
Copy the code

In the code above, in addition to output, awaiting. Js defaults to output a Promise object (the async function immediately executes and returns a Promise object) from which to determine if the asynchronous operation is over.

Here is a new way to load the module.

// usage.js
import promise, { output } from "./awaiting.js";

function outputPlusValue(value) { return output + value }

promise.then(() = > {
  console.log(outputPlusValue(100));
  setTimeout(() = > console.log(outputPlusValue(100)), 1000);
});
Copy the code

In the code above, put the output of the awaiting.js object inside promise.then(), which ensures that the output is read only after the asynchronous operation completes.

This is cumbersome, requiring the user of the module to follow an extra protocol to use the module in a particular way. Once you forget to load with promises and just use the normal loading method, the code that relies on this module may break. Also, if the usage.js file above has an external output, all modules equal to the dependency chain should be loaded using Promise.

The top-level await command addresses this problem. It guarantees that the module will not output values until the asynchronous operation completes.

// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);
Copy the code

In the above code, both asynchronous operations are output with await command. The module will not output values until the asynchronous operation is complete.

Loading the module is written as follows.

// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100));
setTimeout(() = > console.log(outputPlusValue(100)), 1000);
Copy the code

The above code is written exactly like normal module loading. In other words, the user of a module does not need to care at all, depending on whether there is an asynchronous operation inside the module, just load normally.

At this point, the module’s load waits for the asynchronous operation on the dependent module (in this example, awaiting completion of the awaiting model.js) before executing the following code, sort of pausing there. So, it always gets the right output, it doesn’t get a different value depending on the loading time.

Note that the top-level await can only be used in ES6 modules, not in CommonJS modules. This is because the CommonJS module require() is loaded synchronously and cannot handle loading if there is a top-level await.

Here are some usage scenarios for the top-level await.

// Import () method loaded
const strings = await import(`/i18n/${navigator.language}`);

// Database operation
const connection = await dbConnector();

// Dependency rollback
let jQuery;
try {
  jQuery = await import('https://cdn-a.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.com/jQuery');
}
Copy the code

Note that if multiple modules containing the top-level await command are loaded, the load command is executed synchronously.

// x.js
console.log("X1");
await new Promise(r= > setTimeout(r, 1000));
console.log("X2");

// y.js
console.log("Y");

// z.js
import "./x.js";
import "./y.js";
console.log("Z");
Copy the code

The above code has three modules, and the last z.js loads X.js and y.js. The printed result is X1, Y, X2 and Z. This indicates that Z.js does not wait for x.js to finish loading before loading Y.js.

The top-level await command is a bit like giving execution of code to other modules to load, and then taking execution back after the asynchronous operation is complete.