2018-07-12

[JavaScript] この先生きのこるための async/await 入門

Webプログラマーはもう JavaScript を避けて通ることができないようです。

今回は ES2017 で新たに登場した async / await 構文について理解するために記事を書きました。

簡単に言うと これらは Promise オブジェクトをうまく扱うための仕組みです。

Promise

これ を知らないことには async, await は理解できません。 かんたんに言うと Promise は 非同期処理の結果が格納されたオブジェクトです。

普段は fetch 等の リクエストを実行するような関数が Promise を返却するので生で触ることはあまり多くありませんが、 この記事では多用していきます 💪

Promise オブジェクトの作成は 第一引数で受け取った関数を実行するような関数を Promise に渡してあげるだけです。 うーん、余計難しくなってしまいました。

たとえば 10秒待つ Promise は以下のように作ります。

promise = new Promise(resolve => setTimeout(resolve, 10000)) // 単位はミリ秒

resolve(値) のように呼び出されることで、 promise初期状態 から 解決状態 に遷移します。 今回は解決状態になるタイミングを制御することで「待つ」を実現しようというわけです。 ただし、上記を実行しただけでは 何も起こりません。

ここで重要になるのが promise.then(関数) メソッドです。 このメソッドは、promise が 初期状態 のときは処理をブロックし、 解決状態 になると処理結果(resolveの実引数)を抽出できます。

例えばこのように書くと 10秒後に test が出力されます。(今回、処理結果は未指定)

promise.then(() => console.log('test'))

インスタンスが生成された瞬間から Promise に渡された 関数はコールされているため、 上記を実行するタイミングによって test が出力されるタイミングは異なります

promise = new Promise(resolve => setTimeout(resolve, 10000)) // すぐに実行したら 10秒待って出力されるし // 10秒後に実行したらすぐ出力される promise.then(() => console.log('test'))

これが Promise の基本です。

fetch api を使ってる人は fetch(url).then ってよく書いてたと思いますが、 あれは Promise オブジェクトのメソッドだったのですね。

メソッドチェーン
  • then() メソッド は Promise オブジェクトを返却することで、 promise.then(f).then(f2).then(f3) のように 無限につなげることができます。

  • fetch api だと fetch(url).then(res => res.json()).then(data => {console.log(data)}) みたいにしてましたよね。

  • 自分で Promise オブジェクトを指定することで、 10秒待って出力、さらに5秒待って出力、みたいなこともできます。

    promise = new Promise(resolve => setTimeout(resolve, 10000)) promise.then(() => { console.log('test1') return new Promise(resolve => setTimeout(resolve, 5000)) }).then(v => console.log('test2'))

async と await

分けて説明したいところですが、別々に利用することはできないため同時に説明します。

async は 関数の前につけて定義し(以後非同期関数という)、 await は 非同期関数のローカルスコープで利用することで処理をブロックします。

await が何を元にブロックするかというと、これが 前述した Promise オブジェクトなわけですね。やっと繋がりました。

info
  • 非同期関数以外のスコープで await すると以下のようなエラーになります。

    • Uncaught SyntaxError: await is only valid in async function
    • 関数がネストしている場合、直近のスコープが非同期関数であることが求められます。
  • await が受け取るのは Promise オブジェクトじゃなくてもエラーにはなりませんが、何も起こりません。

    • 代入文の場合、左辺にそのまま格納されます。
  • Promise にわたす関数の 第一仮引数名は resolve とするのが慣例らしいです。

  • resolve の第一実引数は await の左辺に渡されます

    hand = v => new Promise(resolve => resolve(v,)); (async () => { let a = await hand('123') console.log(a) let b = await hand('456') console.log(b) })()

await が Promise オブジェクトを受け取ると オブジェクトの状態が resolved になるまでブロックし、以降のプログラムは待ち状態になります。

これを利用して sleep 関数を書いてみましょう。

sleep = time => new Promise(resolve => setTimeout(resolve, time)); (async () => { // 非同期関数を即時実行しているだけ await sleep(3000) console.log(1) await sleep(5000) console.log(2) // 返却値が await に渡されてるだけなので以下のように書いても同じ // await new Promise(resolve => setTimeout(resolve, 5000)) })()

上記を実行すると 3秒待って 1, 5秒待って 2 が表示されましたね。

warning
  • 旧石器時代には開始と終了時刻の差分が指定時間を満たすまで while でループさせるような方法もあったようですが、 あれは CPU パワーをフルに使うのでタブ(あるいはブラウザ)がフリーズします。
  • 最近は期待通りの挙動をしないプログラムをサイトに配置すると起訴されるみたいなので気をつけましょう。

ちなみに fetch api の fetch(url).then(res => res.json()).then(data => {console.log(data)}) は await を使うと 以下のように書けます。

res = await fetch(url) data = await res.json()

then() がなくなり、コールバック関数の中でしか得られなかった data が 同じスコープで 得られるのです。

ちょっと待ってください。 async は await を包むためのただの ラッパーなのでしょうか?

実は async は その名の通り 非同期関数なので、 関数の中は (await がある限り) 同期的に処理をしますが、 関数の外では (awaitに差し掛かると) 非同期に働きます。

f = async (i) => {await null; console.log(i)} f(1) console.log(2) f(3) console.log(4)

これを実行すると 2, 4, 1, 3 の順番で表示されるはずです。 (await がないと 1, 2, 3, 4)

これを同期的に処理したい場合、さらに 非同期関数の結果で await すればいいです。これは 非同期関数が Promise を返却するからです。

(async () => { f = async (i) => {await null; console.log(i)} await f(1) console.log(2) await f(3) console.log(4) })()

そのために さらなる 非同期関数で囲む必要があるのは少し面倒ですけどね。

map の callback として使いたい

Webアプリケーションでは「負荷を軽減するために リクエストは間隔を空けて送信する」みたいな仕様があるかもしれません。

それに見立てて、 ['a', 'b', 'c'] を 3秒間隔で イテレーションしながら 表示するプログラムを考えてみましょう。

まず、以下のように考えました。

sleep = time => new Promise(resolve => setTimeout(resolve, time)) list = ['a', 'b', 'c'] list.map(async v => { await sleep(3000) console.log(v) })

じつはこれ、期待通りに動きません。

理想
  • 開始
    • (3秒待つ)
      • a が表示
        • (3秒待つ)
          • b が表示
            • (3秒待つ)
              • c が表示
  • のように直列処理されてほしかったんですが
現実
  • 開始
    • (3秒待つ)
      • a が表示
    • (3秒待つ)
      • b が表示
    • (3秒待つ)
      • c が表示
  • のように並列処理され、3秒後に同時に表示されてしまいます。現実は厳しい。

まぁ非同期関数の性質を思えば仕方のないことです。

解決方法は以下です。

Solution - for

map によるイテレーションではなく for を使う方法です。

普通の for はだるいので ここでは for of を使います (ES2017の構文)

list = ['a', 'b', 'c']; (async () => { for (let v of list) { await sleep(3000) console.log(v) } })()

これが一番楽かなー。関数型の記述が好きな人にとっては嫌だったりするのかね?

これを非同期関数内部に閉じ込めることで 同期的な map 処理(等)を実現したライブラリを作った方がいるようです。えらい。 GitHub - toniov/p-iteration: Utilities that make array iteration easy when using async/await or PromisesUtilities that make array iteration easy when using async/await or Promises - GitHub - toniov/p-iteration: Utilities that make array iteration easy when using async/await or Promiseshttps://github.com/toniov/p-iteration

Solution - Promise.then

async/await から少し離れて Promise で解決する方法を考えてみましょう。

list = ['a', 'b', 'c']; promise = sleep(3000) list.map(v => { promise = promise.then(() => { console.log(v) return sleep(3000) }) return promise })

関数の外側に定義した promise 変数を then の返却値で上書きしながら コールバックをつなげていきます。 一応期待通りの出力はされるようです。

少しダサいのが難点。うん、無難に for 使いましょう。

参考

Promise - JavaScript | MDNPromise オブジェクトは、非同期処理の完了 (もしくは失敗) の結果およびその結果の値を表します。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise await - JavaScript | MDNawait 演算子は、async function によって Promise が返されるのを待機するために使用します。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/await 非同期関数 - JavaScript | MDN非同期関数は async キーワードで宣言され、その中で await キーワードを使うことができます。 async および await キーワードを使用することで、プロミスベースの非同期の動作を、プロミスチェーンを明示的に構成する必要なく、よりすっきりとした方法で書くことができます。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function async/awaitやPromiseで簡単に配列のイテレーションできるようにする - Qiita作ったモジュールをこの記事で紹介しようと思います。 GitHub: p-iteration (Promise-Iteration) 問題 async/awaitは非常に便利で、コードは同期処理の書き方に近くなったが、ストレートに...https://qiita.com/toniov/items/127267fb64a960e8166e