ES2017のasync/awaitのキソ練習

最近(2018-05現在)はライブラリで Promise 前提のものが増えてきてるしサンプルにもしれっと普通の顔で async/await が登場するので慣れておかないといけない。特に Node.js ではもう LTS である 8 (正確には 7 の途中)で標準的に使えるので、今後はもっと当たり前に登場してくる機会が増えてくることが予想される。1

ということで今回はasync/awaitの練習。ただし await の入れ子や Promise.all で束ねるといった複雑なことは扱っていない。

※ Promise.all() の話は async/awaitやPromiseで気をつけること - あーありがち(2018-06-15) で!

またここで扱っているのはあくまで ES2017 の async/await であって他の独自に実装された async ライブラリのことでもないです。確認は素の Node 8 で行ってます。

まずはPromise

雰囲気だけならこちらをどうぞ。 jQuery.deferredを経由しつつES2015に入ったPromiseの雰囲気を味わう - あーありがち(2016-06-19)

async/await の理解には Promise の理解が欠かせない。なぜなら async/await はあくまで Promise を同期的に、手続き的な処理のように書けるだけだから。

さて。

まず Promise がやってくれるのは非同期処理を抽象化し、統一インターフェイスを提供すること。

プロミスはいつ生成されるかわからない値のプロキシのようなものであり、成功したり失敗したりする動作を扱う時の理想的なパターンです

JavaScriptにおける非同期パターン #翻訳 « Tatyusa's Note

例えば Node.js 標準の callback 関数の受け取り方と jQuery のそれはルールが異なる。2これが ES6 Promise では then(), catch() に制約されるし、Promise は 定義された状態しか持たない。そのうちの一部が fulfilled や rejected である。

つまり、「Promise を返す」とさえ言えばあとは使い方は「Promise を勉強してくれ」で済む。実装者間の共通言語、インターフェイスとして非常に優れている。(ただし学習コストは一時的には上がる。)

Promiseの例

Promise は上にあるように非同期処理に特化したものではない。

単なる即値であっても Array であっても Promise オブジェクト足り得る。例えば以下のような超短いコードも Promise オブジェクトではある。

const p1 = Promise.resolve('a')

これを console.log すると

Promise { 'a' }

と表示される。よく分からないけど 'a' を返してくれる Promise なんだなという雰囲気は伝わってくる。

実際に欲しいのは Promise じゃなくて値だ? ごもっとも。そのためには arrow function を使えば以下のように非常に短い記述で取り出せる。3

p1.then(data => console.log(data))

async functionとawait演算子

まずは基本的なことから

  • async function は Promise を返す
    • Promise を return していない場合は自動的に補完される
    • ただし callback を Promise に変換するのは自分で書くべき

例えば上の Promise は async function を使うと以下のように書ける。

async function asyncValue() {
  return 'a'
}

正確には Promise を返す function を定義しただけなので、同じ挙動をするように手直しをすると、関数の即時実行を使って以下のようになる。

const p2 = (async function() { return 'a' })()

だるいですね。arrow function でこうしちゃう。

const p2 = (async () => 'a')()

callbackを受け取るものは明示的にPromiseに組み直す

async function は確かに自動で Promise を返すんだけど、setTimeout などの callback を受け付けるものは自分で wrap しないと resolve 時に何が渡ってくるのか分からないので、以下のように変換する。

async function lazyA() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('a')
    }, 1000)
  })
}

この場合、async はあってもなくても Promise を返す。Promise が二重化することはなくて、Promise を返すものはそのままスルーしてくれる。ということで、

Promise を返すものは慣習として async を頭に付けちゃう

という運用でよいような気がする。(だったらなくてもいんじゃね?と思うかもしれないが、以下の理由により、逆の運用の方がよいのだ。)

awaitは「解決済み」の値を取り出す演算子

解決済みまで待つので例えば上の lazyA() を例にとると

await lazyA()

とすると then() とか面倒なことしなくても 'a' を取り出すことができる。

ただしawaitは自由には使えない

  • async の中でしか使えない

逆に言うと async function の中では他の async function を await で呼べるということである。

  • 解決済みの Promise の値をオブジェクトとしてメソッドチェインはできない
(async () => {
  const a = await lazyA()
})()

は正しく 'a' になる。しかし以下のようには書けない。

(async () => {
  (await lazyA()).length
})()

つまり以下のようにも書けない。

(async () => {
  (await lazyArray()).map(e => ..)
})()

こういう書き方にしようとすると結局 Promise が顔を出してくる。

await

  • JavaScriptMDN</a>

ここで

[rv] = await expression;

と書かれているのはどうも「値を受け取る人」が必要っぽい。

awaitの後ろは切る

ということで同期処理を始める前に await の結果は代入して処理は切れるようにしておく。以下のような感じ。

(async () => {
  const arr = await lazyArray()
  arr.map(e => e * 2)
})

ただし後ろに繋げるのはダメだが、前ならよい。こんな感じ。(はっきり代入ではないが、扱いとしては同じっぽい。)

let newVal = []

for ( let e on lazyArray ) {
  newVal.push(e * 2)
}

参考

  1. Node 8 が出たのはちょうど1年前の2017年5月です! 

  2. jQuery では使いたい API によっても異なる。 

  3. ただし取り出しが then() の中の callback という形になってしまう。Promise は callback 地獄を解決してくれるのでは?という気持ちになる部分でもある。 

More