Help us understand the problem. What is going on with this article?

非同期APIをPromiseでラップしてasync/awaitで使う

setTimeout と Web Speech API を Promise でラップする例を示します。

Promise とメソッドチェーンだけでは何がやりたいのか分かりにくいかもしれませんが、async/await もセットで考えることで狙いが分かりやすくなると思います。

※ Web Speech API の例は単純なので、触れたことがなくても分かると思います。

setTimeout

Promise の例としては定番です。

コールバック

非同期 API にコールバックを渡す処理をネストすると、いわゆるコールバック地獄になります。

setTimeout によって1秒後に1、その2秒後に2、その更に3秒後に3を表示する例です。

setTimeout(() => {
    console.log(1);
    setTimeout(() => {
        console.log(2);
        setTimeout(() => {
            console.log(3);
        }, 3000);
    }, 2000);
}, 1000);
実行結果
1
2
3

コールバックが深くなるのに加えて、待機時間と後続の処理との対応が分かりにくいです。

※ このように後続の処理を次々とコールバックで渡す書き方を継続渡しスタイル(CPS)と呼びます。

Promise

setTimeout を Promise でラップします。resolve は成功したとき、reject は失敗したときのコールバックです。この例では失敗は考慮しないため reject は無視します。

コールバックは .then で渡します。

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

wait(1000).then(() => {
    console.log(1);
    wait(2000).then(() => {
        console.log(2);
        wait(3000).then(() => {
            console.log(3);
        });
    });
});

これだけだと setTimeoutwait().then に置き換わっただけで、あまりメリットはありません。(待機時間と後続の処理との対応が多少分かりやすくなったくらいでしょうか)

コールバックから Promise を返すように書き換えることができます。

wait(1000).then(() => {
    console.log(1);
    return wait(2000);
}).then(() => {
    console.log(2);
    return wait(3000);
}).then(() => {
    console.log(3);
});

コールバックのネストが .then によるメソッドチェーンに変わりました。ネストが深くなることはなくなり、多少読みやすくなりました。

最初のネストした wait().then の書き方と比較すれば、return で Promise を返してコールバックを抜けてから .then でつなぐことで、ネストをフラットにしていることが分かります。(そこが分からないと、どうやって読むのかピンと来ないかもしれません)

async/await

Promise を使いやすくするための糖衣構文です。wait の定義はそのままです。

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

(async function () {
    await wait(1000);
    console.log(1);
    await wait(2000);
    console.log(2);
    await wait(3000);
    console.log(3);
})();

同期的な処理のように記述することができます。await は「Promise のコールバックが発生するまで待つ」くらいに解釈すれば良いでしょう。awaitasync function(非同期関数)の中でしか使えないため、即時実行関数式にしています。

こうして見ると、Promise は非同期 API と async/await の間を取り持っていることが良く分かります。

※ async/await が糖衣構文だというのは、await の次の行から先がコールバックに変換されて、Promise の中に入っている setTimeout には resolve として渡されることを意味します。(このような変換を CPS 変換と呼びます)

co

async/await がない時代に、同じようなことをジェネレーターで実装した co というライブラリがあります。

co の簡易版を実装して比較します。(説明に必要な範囲に限定しています)

【参考】ES2017におけるasyncとgenerator、Promise、CPS、モナドの関係

function co(g) {
    let it = g();
    function f() {
        let result = it.next();
        if (!result.done) return result.value.then(f);
    }
    return f();
}

co(function* () {
    yield wait(1000);
    console.log(1);
    yield wait(2000);
    console.log(2);
    yield wait(3000);
    console.log(3);
});

async/await が模倣できていて面白いです。

co は async/await が正式にサポートされるまでのつなぎだったようで、今となっては使う必要はなさそうですが、発想は参考になります。また、Promise を検索すると co が現役だった当時の記事が出て来るため、async/await に読み替えられるということを知っておいても損はないでしょう。

【参考】ES7 async/awaitと、coがPromiseベースになっていたこと

もしasync/awaitが実装されれば、coからそのまま乗り換えることも可能になるだろう。

Web Speech API

setTimeout では失敗を考慮しませんでしたが、Web Speech API を例に失敗のあるケースを示します。

Web Speech API によってブラウザでテキストの読み上げを行います。読み上げは非同期的に行われますが、終了を待たずに次々指示しても、キューに溜まって順番に処理されます。

function speak(lang, text) {
    let u = new SpeechSynthesisUtterance(text);
    u.lang = lang;
    speechSynthesis.speak(u);
}

speak("en", "Hello, world!");
speak("fr", "Bonjour, monde !");
speak("ja", "こんにちは、世界!");

連続で読み上げるだけであれば終了を待つ必要はありませんが、もし途中で失敗しても無視して次に進んでしまいます。

コールバック

成功した時だけ次に進むようにするため、終了を待ってコールバックで次の読み上げを指示します。

function speak(lang, text, end, error) {
    let u = new SpeechSynthesisUtterance(text);
    u.lang = lang;
    u.onend = end;
    u.onerror = error;
    speechSynthesis.speak(u);
}

speak("en", "Hello, world!", () => {
    speak("fr", "Bonjour, monde !", () => {
        speak("ja", "こんにちは、世界!", () => {
        }, e => console.log(e));
    }, e => console.log(e));
}, e => console.log(e));

失敗した時は例外を表示します。同じ例外処理を何度も書いています。

※ 一般に e => console.log(e) のように引数を別の関数に取り回すだけのラムダ式は、引数を省略して console.log とも書けます(これをポイントフリースタイルと呼びます)。今回は async/await に書き換える際に e が出て来るため、引数を明示した形で進めます。

Promise

setTimeout では無視した reject を失敗時のコールバックとして使います。失敗時の処理は .catch で与えます。

まずはネストのまま書き換えます。

function speak(lang, text) {
    return new Promise((resolve, reject) => {
        let u = new SpeechSynthesisUtterance(text);
        u.lang = lang;
        u.onend = resolve;
        u.onerror = reject;
        speechSynthesis.speak(u);
    });
}

speak("en", "Hello, world!").then(() => {
    speak("fr", "Bonjour, monde !").then(() => {
        speak("ja", "こんにちは、世界!").then(() => {
        }).catch(e => console.log(e));
    }).catch(e => console.log(e));
}).catch(e => console.log(e));

これをフラットにします。何もしない最後の .then を省略して、失敗時の処理を1つにまとめます。

speak("en", "Hello, world!")
    .then(() => speak("fr", "Bonjour, monde !"))
    .then(() => speak("ja", "こんにちは、世界!"))
    .catch(e => console.log(e));

かなりすっきりしました。

最初の speak だけ書き方が異なりますが、ダミーの Promise から始めることで揃えられます。

new Promise((resolve, reject) => resolve())
    .then(() => speak("en", "Hello, world!"))
    .then(() => speak("fr", "Bonjour, monde !"))
    .then(() => speak("ja", "こんにちは、世界!"))
    .catch(e => console.log(e));

ダミーの Promise を生成する簡単な方法があります。

Promise.resolve()
    .then(() => speak("en", "Hello, world!"))
    .then(() => speak("fr", "Bonjour, monde !"))
    .then(() => speak("ja", "こんにちは、世界!"))
    .catch(e => console.log(e));

【参考】Promiseを複数組み合わせる時の基本パターン(直列、並列、分岐)

最初にダミーのPromise.resolve()を入れるようにしています。

async/await

メソッドチェーンを非同期の即時実行関数式で書き換えます。speak の定義は Promise と同じため省略します。

(async function () {
    try {
        await speak("en", "Hello, world!");
        await speak("fr", "Bonjour, monde !");
        await speak("ja", "こんにちは、世界!");
    } catch (e) {
        console.log(e);
    }
})();

trycatch によって同期的な処理に近い見た目で記述できました。

co

参考までに co 版を書いておきます。

co(function* () {
    yield speak("en", "Hello, world!");
    yield speak("fr", "Bonjour, monde !");
    yield speak("ja", "こんにちは、世界!");
}).catch(e => console.log(e));

まとめ

Promise で成功と失敗のコールバックに振り分けてラップすることで、async/await によって表記が簡略化できます。

参考

Promise の詳細は MDN の記事が参考になります。「古いコールバック API をラップする Promise の作成」のセクションは今回の記事と関係があります。

Web Speech API

Web Speech API の使い方は以下の記事を参照してください。

Promise と組み合わせた応用的な使い方は以下を参照してください。

モナド

コールバックから async/await への書き換えは、Haskell での >>= を明示した書き方から do ブロックへの書き換えに対応します。

JavaScript のジェネレーターでモナドを実装している記事です。co の方式とも密接な関係があります。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした