uniquejava / blog

My notes regarding the vibrating frontend :boom and the plain old java :rofl.
Creative Commons Zero v1.0 Universal
11 stars 5 forks source link

node.js async/await #224

Open uniquejava opened 6 years ago

uniquejava commented 6 years ago

async/await本来就是基于promise, 它不是要替代promise, 而是要替代callback.

写得zei好, 转自: Working with JavaScript callback APIs from async/await

To ignore Node.js as a possibility in certain problem domains, for which it is the best tool for the job, is a tremendously silly and at times unprofessional decision. While I don't delight in writing JavaScript, I must acknowledge that JavaScript has matured quite nicely over the past ten years. Perhaps the most helpful addition, for me at least, are the async and await keywords which aim to prevent the callback nightmare many casual JavaScript developers may dread.

Particularly for Node applications, callbacks provided a mechanism through which highly event-driven code could be executed. Inside the runtime, this generally means the execution thread can defer certain slow operations, such as timers or network I/O, until the timer fires or the socket's buffer has data available for the application. All the while, executing other "work" within the application. I was first introduced to this cooperative multitasking approach over a decade ago, via "greenlets" in Python, the tools and libraries I used were hacks on top of CPython, and never caught any significant adoption. Node, however, is "just JavaScript" which practically every web application must maintain some familiarity with anyways. This allowed Node to enter a niche, which Go would later intrude upon, of lightweight and high-connection-count services.

Unfortunately, callback-oriented code is fairly difficult to read and understand as it's execution-flow cannot be read linearly by scrolling down in the text editor. For this reason, in my opinion, the async and await syntax sugar is so valuable in JavaScript. Borrowing from javascriptasyncfunction.com, callback-oriented code such as:

function foo(onSuccess) {
  var request = new XMLHttpRequest();
  request.open('GET', 'https://swapi.co/api/people/1/', true);

  request.onload = function() {
    if (request.status >= 200 && request.status < 400) {
      var data = JSON.parse(request.responseText);
      onSuccess(data.name);
    }
  };

  request.send();
}

Can be re-written as:

async function foo() {
  const response = await fetch('https://swapi.co/api/people/1/');
  const parsedResponse = await response.json();
  return parsedResponse.name;
}

This is all well and good, but only works because the APIs underneath, e.g. fetch, have been introduced to support it. For the unfortunate developer (read: me) who must work with the legacy "callback-oriented" APIs, it might not be obvious how to use async and await in an application which must integrate with callback-driven libraries.

While banging my head against this problem I learned that JavaScript engines introduced the Promise API, which was somehow related, but it was never succinctly clear how.

What I found so terribly confusing was: I had always seen the async and await keywords used together but never with a callback-oriented API.

It helps to tease the two apart, and explain them separately:

async: should be used with a function declaration to denote that it can be deferred and will, in effect, implicitly return a Promise.

await: should be used to block a sequential flow of execution until a Promise can be resolved. await cannot be used unless the function containing it is marked async.

Let's say I want to take a function, which currently uses callbacks, and incorporate it into the rest of my async/await application. The trick, it turns out, is to wrap it with a Promise:

function sendMessage(payload) {
    return new Promise((resolveFunction, rejectFunction) => {
        clientAPI.send(payload, (error, response) => {
            /* in the callback */

            /* if there was an error, invoke the `reject` function as part of
               the Promise API. */
            if (error) { rejectFunction(error); }

            /* if there was a response, inoke the `resolve` function as part of
               the Promise API */
            resolveFunction(response);
        });
    });
}

This sendMessage function can then be used in other async type functions, e.g.:

async function notifyBroker() {
    let response = await sendMessage({ping: true});
    /* do something with `response` */
}

This doesn't completely change the writing of JavaScript to a sequential model, the top-level invocation of this function must treat it as a Promise, e.g.: notifyBroker().then(() => { /* callback when notifyBroker() completes */ });

It does, however, make it a lot easier to author non-blocking code without a descent into callback hell.

uniquejava commented 6 years ago

6 Reasons Why JavaScript’s Async/Await Blows Promises Away (Tutorial)

怎么将已有的基于callback的API变成promise

How do I convert an existing callback API to promises?

util.promisify

在node.js v8中加了util.promisify函数.

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);
readFile('./notes.txt')
  .then(txt => console.log(txt))
  .catch(...);

doAsync

In Node.js 8 you can promisify object methods on the fly using this npm module:

https://www.npmjs.com/package/doasync

It uses util.promisify and Proxies so that your objects stay unchanged. Memoization is also done with the use of WeakMaps). Here are some examples:

With objects:

    const doAsync = require('doasync');
    doAsync(fs).readFile('package.json', 'utf8')

With functions:

    doAsync(request)('http://www.google.com')

You can even use native call and apply to bind some context:

    doAsync(myFunc).apply(context, params)
      .then(result => { /*...*/ });

Bluebird, Q, ...

var getStuffAsync = Promise.promisify(getStuff); // Bluebird
var getStuffAsync = Promise.promisifyAll(API); // Bluebird
var getStuffAsync = Q.denodeify(getStuff); // Q
var getStuffAsync = util.promisify(getStuff); // Native promises, node only
uniquejava commented 6 years ago

实战

async function add(a, b) {
  return a + b;
}

add(1, 2).then(value => console.log(value));

let sum1 = add(1,2)
console.log('sum1=', sum1); // 这里的sum1不是3, 而是Promise(3)

let sum2 = await add(1, 2); // 错误, 这里不能用await
console.log('sum2=', sum2);

总结:

  1. async能将任意函数的返回值自动变成promise.
  2. await只能用在标记成async的函数中, 上面代码中的await用法是错误的. 如果函数未被标记为async, 编译器会提示不识别的符号await.
  3. await会自动resolve promise
  4. reject只能用try/catch捕获.

forEach中的async/await不起作用

Using async/await with a forEach loop

有问题的代码

import fs from 'fs-promise'

async function printFiles () {
  const files = await getFilePaths() // Assume this works fine

  files.forEach(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  })
}

printFiles()

解决方法: Sure the code does work, but I'm pretty sure it doesn't do what you expect it to do. It just fires off multiple asynchronous calls, but the printFiles function does immediately return after that.

Reading in sequence

If you want to read the files in sequence, you cannot use forEach indeed. Just use a modern for … of loop instead, in which await will work as expected:

    async function printFiles () {
      const files = await getFilePaths();

      for (const file of files) {
        const contents = await fs.readFile(file, 'utf8');
        console.log(contents);
      }
    }

Reading in parallel

If you want to read the files in parallel, you cannot use forEach indeed. Each of the async callback function calls does return a promise, but you're throwing them away instead of awaiting them. Just use map instead, and you can await the array of promises that you'll get with Promise.all:

    async function printFiles () {
      const files = await getFilePaths();

      await Promise.all(files.map(async (file) => {
        const contents = await fs.readFile(file, 'utf8')
        console.log(contents)
      }));
    }