はじめに
表題のモチベーションで書き上げた備忘録に加筆・修正したものを記事にしました。
記事を読んで下さった方の Promise, async/await の理解度が 1LV でもアップしてくれたら嬉しいなと思います。
Promise とは
- Promise は非同期関数を扱うためのインターフェース
- Promise は 悲運のピラミッド型コールバック (ネスト地獄)を根本的に解決してくれる
- thenable と呼ばれる then メソッドを有するオブジェクトを resolve の第一引数に入れることで Promise オブジェクトへ変換することが可能
// thenable を Promise オブジェクトへ変換する
const converted = Promise.resolve({
then: (onFulfilled) => return onFulfilled('be promise')
})
converted.then(value => console.log(value)) // be promise
async/await とは
- async/await は Promise のシンタックスシュガーとして用いられる
- async/await は Promise を同期的なコードのように書ける
Promise の書き方とメソッドの簡単な説明
Promise の最も基本的なコード
const promise = new Promise((resolve, reject) => resolve('解決した'))
const unResolvedPromise = new Promise((resolve, reject) => reject('エラー発生'))
promise
.then((value) => value) // 解決した
unResolvedPromise
.then((value) => value)
.catch((errorMessage) => new Error(errorMessage)) // error: エラー発生
.then((value) => value) // エラー発生
then と catch
then と catch はプロミスにおける最重要事項の一つです。これらは promise を返すため、メソッドチェーンを繋げることができます。
then
Promise#then(onFulfilled, onRejected)
- Promise に成功ハンドラ(onFulfilled) と失敗ハンドラ(onRejected) を付加する
- 2つの引数を持ち、それらはそれぞれ Promise の状態が Fulfilled の時と Rejected の時のコールバック関数である
- 返り値は promise
onFulfilled(value)
- 一つの引数 value を持つ
- value は promise にラップされた値など
onRejected(reason)
- 一つの引数 reason を持つ
- reason は rejected となった理由など
catch
Promise#catch(onRejected)
-
then(undefined, onRejected)
のショートカットとして機能する - 返り値は promise
- 引数の onRejected は then と参照
静的メソッド
Promise.resolve
下記コードのエイリアスです。渡した値で Fulfilled される promise オブジェクトを返します。
new Promise((resolve) => resolve(/* value */))
Promise.reject
下記コードのエイリアスです。
new Promise((resolve, reject) => reject(/* reason */))
Promise.all
- 引数に Promise 関数の配列を取る実行
- 返り値は Promise
- 引数に取った Promise で一つでもエラーが発生した場合はその rejected されたエラーが返る
- エラーが発生しなかった場合は「value にそれぞれの resolve の結果が入った配列」の Promise が返る
Promise.all(promises).then(allFulfilled, firstRejected)
Promise.race
- 引数に Promise 関数の配列を取り実行する
- 引数にとった Promise 関数のいずれかが fullFilled または rejected になった時点でその Promise を返す
Promise.race(promises).then(firstFulfilled, firstRejected)
Promise の仕様的なところ
async/await をちゃんと使えるようになるには Promise が内部でどう働いているかある程度わかっている必要があります。
Promise は状態をもつ
インスタンス化された promise のオブジェクトには以下の 3 つの状態が存在します。
- Fulfilled
- Rejected
- Pending
- promise のオブジェクトの状態は不変である
- 一度 Fulfilled か Rejected になったら Pending には戻らない
- Fulfilled, Rejected は Settled(落ち着いた状態, 不変の状態)ともいう
- Pending から Settled になったら状態はもとに戻らない
渡された関数の実行タイミング
- すぐに実行されるのではなく、マイクロタスクのキューに入れられる
- このキューは現在のイベントループの終わりに空になる
async/await の書き方
console.log(101)
するコードを (1)then catch, (2)async/await の2パターン書きました。
doSomething
関数内でエラーが発生した場合は failureCallback
にエラーが渡されてログに出すようにしています。
then と catch を使った場合
const doSomething = () => Promise.resolve(100)
const failureCallback = (e) => console.log(e)
doSomething()
.then(v => v + 1)
.then(v => console.log(v)) // 101
.catch(failureCallback);
async/await を使った場合
// ※ 即時関数で囲っています
const doSomething = () => Promise.resolve(100)
const failureCallback = (e) => console.log(e)
(async () => {
try {
let result = await doSomething()
result = result + 1
console.log(result) // 101
} catch (e) {
failureCallback(e)
}
})();
サンプルコード集
筆者の場合 Promise, async/await は説明やコードを読んでいるだけではいまいち理解できなかったので、実際にコードを書いて動かして理解を深めました。その過程で特に残しておきたい(忘れた時に後から見たい)と思ったコードを載せております。
古いコールバックを使った API をラップする
setTimeout をラップします。これはコールバックのAPIをラップしたもっともシンプルな例かもしれません。
// setTimeout を Promise にする
const wait = ms => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
wait(1000).then(() => console.log('logged after 1000ms'))
複数の関数を直列に合成する
const composeAsync = (...funcs) => {
return x => {
return funcs.reduce((acc, val) => {
return acc.then(val)
}, Promise.resolve(x))
}
}
// 使用例
const succ = n => n + 1
const multiply = x => y => x * y
const transformData = composeAsync(succ, succ, multiply(3))
transformData(2)
.then(v => console.log(v)) // 12
async/await と Promise のコードを比較してみる
async function 内で console.log(100)
、async function を呼ぶときに console.log(100)
するコードで比較してみます。両コードの挙動は全く同じです。
async/await
使った方が圧倒的に書きやすく、またコードを見た時にどういう動きをしそうか、直感的に理解しやすいと思いました。
async/await
const ret100 = () => Promise.result(100)
const asyncFunc = async () => {
let result = await ret100()
result += await ret100()
console.log(result) // log.1 200
return result // Promise を返す
}
asyncFunc()
.then(value => console.log(value)) // log2. 200
Promise
const ret100 = () => Promise.result(100)
const asyncFuncPromise = () => {
let result = 0
return ret100()
.then(value => {
result += value
return ret100()
})
.then(value => {
result += value
console.log(result) // log.1 200
return result
})
}
asyncFuncPromise()
.then(value => console.log(value)) // log.2 200
async/await のエラー処理の記述パターン
下記はサンプルコード内で使用している errorPromise 関数です。
const errorPromise = () => {
return new Promise((resolve, reject) => {
try {
throw new Error('Some error occured')
resolve('no error')
} catch (err) {
reject(err)
}
})
}
1. try/catch を使う
const asyncFuncTryCatch = async () => {
try {
const result = await errorPromise()
console.log(result) // unreachable
} catch (err) {
console.log(err)
}
}
asyncFuncTryCatch() // log. Some error occured
2. try/catch を使わない
const asyncFuncNoTryCatch = async () => {
const result = await errorPromise()
.catch(err => err)
console.log(result)
}
asyncFuncNoTryCatch() // Some error occured
3. async function 内でエラーハンドリングしない
const asyncFuncHandleErrorOut = async () => {
const result = await errorPromise()
console.log(result) // unreachable
}
// async function を呼ぶ時にエラーハンドリング
asyncFuncHandleErrorOut()
.catch(err => console.log(err)) // Some error occured
おまけ. Promise のエラーハンドリング
const asyncPromiseFunc = () => {
return errorPromise()
.then(value => value)
.catch(err => err)
}
asyncPromiseFunc() // Some error occured
非同期関数を並列で実行させたいとき
非同期関数を並列で実行させたいときは Promise.all
を使用する必要があります。
例えば、下記のようなコードがあり、 getFood
と getDrink
を並列で実行し、両関数が完了した後に orderItems
を実行したいとします。
const getFood = () => {
return new Promise(resolve => {
setTimeout(resolve, 1000, 'got food')
})
}
const getDrink = () => {
return new Promise(resolve => {
setTimeout(resolve, 1000, 'got a drink')
})
}
const orderItems = (...items) => {
return new Promise(resolve => {
setTimeout(() => {
items.forEach((msg) => console.log(msg))
}, 2000)
})
}
NGパターン
下記コードは getFood
> getDrink
> orderItems
のように直列で実行されてしまいます。
const myOrder = async () => {
const msg1 = await getFood()
const msg2 = await getDrink()
orderItems(msg1, msg2)
}
myOrder()
OKパターン(1)
(getFood
, getDrink
) > orderItems
のように並列で実行します。
NGパターン(1) 同様 getFood
> getDrink
> orderItems
のように直列で実行されます。
(@azu さん 指摘して頂きありがとうございます。)
※修正:20180501.1745
改めて確認したところ、(getFood
, getDrink
) > orderItems
のように並列で実行されるようでした。
但し、 OKパターン(2) のように Promise.all を使用するのが一般的のようなので、そちらを使うのがベターと思われます。
const myOrder = async () => {
const msg1 = getFood()
const msg2 = getDrink()
orderItems(await msg1, await msg2)
}
myOrder()
OKパターン(2)
(getFood
, getDrink
) > orderItems
のように getFood
と getDrink
は並列で実行されます。一般的に使われているのはこちらのパターンのようです。
const myOrder = async () => {
const msg1 = getFood()
const msg2 = getDrink()
const msgs = await Promise.all([msg1, msg2])
orderItems(...msgs)
}
myOrder()
おわりに
以上です。
async/await にしろ Promise にしろ、ドキュメントや記事(この記事のような)を読むだけでなく、自分で手を動かしトライ&エラーを繰り返すことって大事だなとあらためて思いました(どの技術の習得もそうですが・・)。
コードや文章の指摘、質問も頂けると助かります。
再度までお読み頂きありがとうございました。
参考資料
- Promise の本 - azu
- Promise/A+ チュートリアル和訳
- Promise を使う - JavaScript | Mozilla
- async function - JavaScript | Mozilla
- await - JavaScript | Mozilla
- async/await 入門 - Qiita
- async/await 地獄 - Qiita
- [フロントエンド] ES7のasync/awaitを使って、Promiseを同期的に処理する - YoheiM.NET
@SotaSuzuki OKパターン1ですが、恐らく並列ではなく直列で処理されると思います。
getFood
とgetDrink
の実行自体はすぐ行われますが、orderItems
が呼び出されるのは、getFood()
のPromiseが解決 ->getDrink()
のPromiseが解決 ->orderItems()
を呼び出すという順番になります。(関数の引数は左から順に処理されので左が解決されるまでは右へ移動しない。single var patternでのカンマ区切りの処理と同じです。)
https://qiita.com/SotaSuzuki/items/f23199e864cba47455ce#ok%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B31
OKパターン1は意味的には次のように書いたのと同じです。
なので、次のような感じになるかなと思います。
2. try/catch を使わない
await
が抜けている気がします。OKパターン1でもgetFoodとgetDrinkは並列に動きますね。
Promise内部のコールバック関数は即座に実行されます。
その後のawaitで直列になります。
@azu さん
指摘して頂きありがとうございます。
上のOKパターン(1)について自分でも試してみたところ直列で実行されているようでした。
指摘して頂いた該当箇所を修正いたしましたので時間がある時にご確認頂けますと幸いです。
@kurogelee さん
コメントありがとうございます。
getFoodとgetDrinkは並列に動くようですが、その後 orderItems 内で引数を処理する際に直列になるようですね。
例としてあまり良いものでは無いかもしれませんね、該当箇所は消してもよい気がしてきました。
@azu さん
再度試してみたところ、以前の OKパターン(1)のような書き方でも並列で実行されるようだったので、記事を再度修正しております。
どうやら await なしで関数を実行したときに
fullFilled
の状態まで処理が進むため、orderItems
を実行したときの引数を処理する時に待つ必要がなくなるのだと思われます。重箱の隅ですが…
若干曖昧な表現なので、下記仕様のように
then メソッドがあるオブジェクトまたは関数とした方が良いのではないかと思います。
更にチェーンで繋ぐには Promise.resolve に渡す必要もあります。
↓こんな感じでしょうか。
あと、
const msgs = await Promise.all(msg1, msg2)
↓
const msgs = await Promise.all([msg1, msg2])
でないとエラーになります。
"コールバックは現在の JavaScript イベントループの実行よりも前には呼び出されない" についてですが
Promiseオブジェクトを作成する際に渡すコールバックについては、現在のイベントループ中に実行されるようです。
ts-node/Node.jsで以下を実行すると数字の順番通りに表示されました。
@chuntaro さん
指摘頂きありがとうございます!件の箇所は曖昧さを回避するよう修正いたしました。
(Promise.all の引数もありがとうございます)
@kurogelee さん
コメントありがとうございます。すぐにコールバック呼ばれてますね。
私もきちんと理解できているわけではなかったため、この "コールバックは現在の JavaScript イベントループの実行よりも前には呼び出されない" という箇所は差し控えることにします。