現代的JavaScriptおれおれアドベントカレンダー2017 – 11日目

概要

Promise を使った非同期の処理をさくさく書けるやつです。

簡単な例としては、まず Promise オブジェクトを返す関数↓があったとして、

// 指定ms待つ
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

このコード↓を

function sayAfterSleeping(message) {
    console.log('ぐー……');
    sleep(1000)
        .then(() => {
            console.log('むにゃ……');
            return sleep(1000);
        })
        .then(() => {
            console.log('あ、ごめん寝てた。えっと……');
            return sleep(1000);
        })
        .then(() => {
            console.log(message);
        })
}

sayAfterSleeping('Hello Async World!');

こう↓書けます。

async function sayAfterSleeping(message) {
    console.log('ぐー……');
    await sleep(1000);

    console.log('むにゃ……');
    await sleep(1000);

    console.log('あ、ごめん寝てた。えっと……');
    await sleep(1000);

    console.log(message);
}

sayAfterSleeping('Hello Async World!');

基本的な使い方

Promise を使ったコードを、前述のようにそれなりに簡単に置き換えることができます。

  1. Promise を返す関数の呼び出しに await を付ける
  2. await を使う関数に async を付ける

これだけです。

忘れないでね

async を付けるのは Promise オブジェクトを返す関数ではなく、 await を使う方の関数という点にご注意ください。逆に Promise オブジェクトを返すやつに async を付ける必要は(その中で await を使っていないなら)ないです。

あ、あと await を書けるのは async を付けた関数の中だけです。

ここら辺最初のころは忘れたり混同したりしがちでは。(個人の感想です。)

結果を受け取る

then() で登録するコールバック関数実行時に結果を受け取ることができましたが、 await の場合は単純な戻り値のような形で扱えます。

// fetch()とformat()はダミーです

function fetchDetail(id) {
    return fetch(`/api/foo/${id}`)  // Promiseを返す
        .then((res) => {
            return format(res);  // 結果を整形
        });
}

async function showDetail(id) {
    const detail = await fetchDetail(id);  // 待ってから結果を受け取る
    console.log(detail.name, detail.type);
}

showDetail(123);

Fetch APIの話じゃないです。

for 文

実践編。

間を置きながら n 回数えて終了する countdown() という処理を考えます。

真ん中が本体です。

// 指定ms待つ
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function countdown(n) {
    let p = Promise.resolve();
    for (let i = 0; i < n; i++) {
        p = p.then(() => {
            console.log(`${n - i}...`);
            return sleep(1000);
        });
    }
    return p;
}

countdown(3)
    .then(() => {
        console.log('done!')
    })

なんかややこしいすね。

単純な for 文だと駄目です。だって Promise 完了まで待たないとすぐ終わっちゃうから。なので、 Promise オブジェクトを変数 p で覚えておいて、さらにそこから then() の戻り値で上書きしながら連結します。(ちなみに let を var にするとだめです。)

これ↑を、 await を使うことで、こう↓めっちゃ簡単に書けるようになります。

// 指定ms待つ
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function countdown(n) {
  for (let i = 0; i < n; i++) {
    console.log(n - i, '...');
    await sleep(1000);
  }
}

countdown(3)
  .then(() => {
    console.log('done!')
  })

ほら、さっき駄目だった「ただの for 文」で済んでしまいました。読みやすーい。

宣言、式、メソッド

各種取り揃えております。

async function foo() {}

const bar = async function() {};

const obj = {
  async hoge() {},
};

class Klass {
  async fuga() {}
}

例外

Promise オブジェクトの catch() は、普通の try-catch で実現できます。

async function() {
  try {
    const result = await doAsync();
  }
  catch (err) {
    console.error(err);
  }
}

いろいろな await

途中に await

わりとどこにでも await を突っ込むことができるようです。

function returnLater(result) {
  return new Promise(resolve => setTimeout(() => resolve(result), 1000));
}

async function af () {
  console.log('...');
  console.log(await returnLater(1) + await returnLater(2));  // 3
}

af();

ただ await 出現の時点で待機を開始するので、複数書いても並列に実行はしません。上記の例では1000 ms待つのを二回呼んでるので、結果が出てくるまで2000 msかかります。

await new Promise()

というのもできます。要は右辺が Promise オブジェクトなら良いわけで、関数実行は結果としてそれを満たすだけです。

async function foo() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('おまたせ!');
}

await 123

というのもできます。右辺が Promise オブジェクトだったら待つんだけど、そうでなければそのまま流します。

await await await p

というようにたくさん並べることもできます。

できるけどしないね。(関数呼び出しの中身を追っていくと結果的にこうなる場面はいくらでもあるだろうけど。)

ES2017

ES2015 (ES6)じゃなくてES2017で追加されました。

ChromeとFirefoxではもう使えます。それ以外で使うならバベってください。

その他

async 後に改行は駄目

だめです。

async
function foo() {}
// Exception: ReferenceError: async is not defined

まあこんなのしないよね。

Function オブジェクトではない

async 付きで宣言したものは、コンストラクタが Function ではなく AsyncFunction になります。

console.log((async function(){}).constructor);  // => function AsyncFunction()

とはいえ AsyncFunction が Function を継承しているので、 instanceof は普通に動きます。ご安心ください。

console.log((async function(){}) instanceof Function);  // => true

AsyncFunction コンストラクタ

前項の通り、 AsyncFunction コンストラクタは存在するんだけど、グローバルオブジェクトにはなってません。

const af = new AsyncFunction();  // Exception: ReferenceError: AsyncFunction is not defined

どうしても使いたければ、インスタンスから探っていきます。

const AsyncFunction = (async function(){}).constructor;
const af = new AsyncFunction();

使い方は Function といっしょ。

“async” の読み方

「アシンク」より「エイシンク」くらいっぽいです。まあ日本語で会話してるところにエィスィ↑ンクとか言っても逆に通じないだろうけど。

英単語 “async”, “await” の意味

“async[hronous]” は「非同期」。皆様ご存知ですね。

“await” は「待ち受ける」だそうです。

参考