はじめに
表題のモチベーションで書き上げた備忘録に加筆・修正したものを記事にしました。
記事を読んで下さった方の Promise, async/await の理解度が 1LV でもアップしてくれたら嬉しく思います。
Promise とは
- Promise は非同期関数を扱うためのインターフェース
- Promise は 悲運のピラミッド型コールバック (ネスト地獄)を根本的に解決してくれる
- thenable と呼ばれる then メソッドを有するオブジェクトを resolve の第一引数に入れることで Promise オブジェクトへ変換することが可能
// thenable を Promise オブジェクトへ変換する
const converted = Promise.resolve({
then: (onFulfilled) => 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.resolve(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.resolve(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