Promise, async, await
個人用のメモなので不正確です。
Promise
console.log('start')
new Promise((resolve, reject) => {
console.log('Promise start')
const value = Math.random()
if(value < 0.5) {
resolve(value)
} else {
reject(value)
}
console.log('Promise end')
}).then((value) => {
console.log('resolveが実行された場合に実行される:', value)
}).catch((error) => {
console.log('rejectが実行された場合に実行される:', error)
}).finally(() => {
console.log('必ず最後に実行される')
})
console.log('end')
実行結果
start
Promise start
Promise end
end
resolveが実行された場合に実行される: 0.4217906883305165
必ず最後に実行される
then
やcatch
の実行は非同期で行われる。しかし、Promise
の引数の関数は即座に実行される。
then
やcatch
はresolveやrejectが呼ばれた時ではなく、それらの関数が呼ばれていて、Promise
の引数の関数の実行が終わっている状態なら非同期で実行される。
then
then
はチェインすることができる。
new Promise((r) => r(1))
.then((v) => v * 2)
.then((v) => v * 3)
.then((v) => console.log(v)) // 6が表示される
catch
catch
はPromise
のrejectが呼ばれた時に実行されるが、例外が発生した場合でも実行される。
new Promise(() => { throw new Error('例外') })
.then(() => console.log('実行されない'))
.catch((error) => console.log(error.message)) // 例外 のみが表示される
then
の中で例外が発生した場合も同様にcatch
が実行される。catch
の中で例外が発生した場合も後続のcatch
が実行される。
new Promise((r) => { r(1) })
.then((v) => { throw new Error('例外') })
.then(() => console.log('実行されない'))
.catch((error) => console.log(error.message)) // 例外 のみが表示される
また、catchから戻り値を返して、後続のthenに繋げることもできる。
new Promise((r) => { r(1) })
.then((v) => { throw new Error('error from then1') })
.then(() => console.log('実行されない'))
.catch((error) => {
console.log(error.message)
return error.message + ' with catch1'
})
.then((v) => { throw new Error(v + ' from then2') })
.then(() => console.log('実行されない'))
.catch((error) => {
console.log(error.message)
return error.message + ' with catch2'
})
.then((v) => console.log('実行される:', v))
// 出力結果
// error from then1
// error from then1 with catch1 from then2
// 実行される: error from then1 with catch1 from then2 with catch2
thenの中でのPromise
例えば、Promiseのresolveの結果を受けて、1つ目のthen
でさらにAjaxリクエストを出して、後続のthen
に繋げたい場合、単に以下のように書いたのでは機能しない。
import https from "https"
new Promise((resolve, reject) => {
// JSONを取ってきているだけ
https.get('https://jsonplaceholder.typicode.com/users/', res => {
let body = ''
res.setEncoding('utf8')
res.on('data', (chunk) => body += chunk)
res.on('end', () => resolve(body)) // 取得結果をresolveに渡す
})
}).then((value) => JSON.parse(value))
.then((value) => {
console.log(value[0].name)
return value[0].name
})
.then((value) => { // 最初のJSONの結果を使いつつ追加でJSONを取得しようとする
let all = value
https.get('https://jsonplaceholder.typicode.com/users/', res => {
let body = ''
res.setEncoding('utf8')
res.on('data', (chunk) => body += chunk)
res.on('end', () => all = body)
})
// 1回目と2回目の結果を合わせたものをreturnしようとしているが
// 実際にはhttps.getが終わる前にreturnされてしまう
return all
}).then((value) => console.log(value))
この問題の解決策は、then
でnew Promise
して返せば良い。
import https from "https"
new Promise((resolve, reject) => {
https.get('https://jsonplaceholder.typicode.com/users/', res => {
let body = ''
res.setEncoding('utf8')
res.on('data', (chunk) => body += chunk)
res.on('end', () => resolve(body))
})
}).then((value) => JSON.parse(value))
.then((value) => {
console.log(value[0].name)
return value[0].name
})
.then((value) => {
return new Promise((resolve, reject) => {
https.get('https://jsonplaceholder.typicode.com/users/', res => {
let body = ''
res.setEncoding('utf8')
res.on('data', (chunk) => body += chunk)
res.on('end', () => resolve(value + JSON.parse(body)[1].name))
})
})
// 最初のリクエストと二回目のリクエストの結果が連結されたものが表示される
}).then((value) => console.log(value))
thenの第二引数
then
の第二引数に関数を指定するとcatch
として機能する。
new Promise((r) => { r(1) })
.then((v) => { throw new Error('例外') })
.then(() => console.log('実行されない'),
(error) => console.log(error.message)) // 例外 のみが表示される
Promise.all
、Promise.allSettled
、Promise.rase
複数のPromiseがあって、すべてresolveされてからthen
を実行したいような場合、Promise.all
を使う。
const p1 = new Promise((r) => setTimeout(() => { r(1); console.log(1) }, 1000))
const p2 = new Promise((r) => setTimeout(() => { r(2); console.log(2) }, 2000))
const p3 = new Promise((r) => setTimeout(() => { r(3); console.log(3) }, 3000))
Promise.all([p1, p2, p3]).then((values) => console.log(values))
// 実行結果
// 1
// 2
// 3
// [ 1, 2, 3 ]
しかし、Promise.all
は一部でもrejectされるとthen
ではなくcatch
が実行される。
const p1 = new Promise((r) => setTimeout(() => { r(1); console.log(1) }, 1000))
// rejectを実行している
const p2 = new Promise((_, r) => setTimeout(() => { r(2); console.log(2) }, 2000))
const p3 = new Promise((r) => setTimeout(() => { r(3); console.log(3) }, 3000))
Promise.all([p1, p2, p3])
.then((values) => console.log(values))
.catch((value) => console.log(value, 'in catch'))
// 実行結果。全てが終わるのを待たず、rejectされた時点でcatchが呼ばれている
// 1
// 2
// 2 in catch
// 3
rejectされたものがあっても、全てのPromiseが終わるのを待ってthen
を実行したい場合はPromise.allSettled
を使う。
const p1 = new Promise((r) => setTimeout(() => { r(1); console.log(1) }, 1000))
const p2 = new Promise((_, r) => setTimeout(() => { r(2); console.log(2) }, 2000))
const p3 = new Promise((r) => setTimeout(() => { r(3); console.log(3) }, 3000))
Promise.allSettled([p1, p2, p3])
.then((values) => console.log(values))
.catch((value) => console.log(value, 'in catch'))
// 実行結果
// 1
// 2
// 3
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 2 },
// { status: 'fulfilled', value: 3 }
// ]
// このようにallSettledを利用した場合、
// 実行ステータスを含むオブジェクトの配列が引数に渡される
Promise.resolve
、Promise.reject
new Promise((r) => r(1))
と書く代わりにPromise.resolve(1)
と記述することができる。rejectの場合も同様にPromise.reject
を利用できる。
Promiseが解決すること
第一に、順序が保証されることがある。resolveされてからしか、then
は実行されないし、後続のthen
も前のthen
が終わってからしか実行されない。
もし、Promiseを使わずに非同期処理を直列に実行しようとした場合、コールバックをネストして渡すしか方法がなく非常に見通しが悪くなる。これが第二の解決点になる。
async
、await
async
はasync () => {}
のように関数やメソッドにつけ、その関数をPromiseを返す関数にする。
await
はawait Promiseインスタンス
という書き方で、Promiseインスタンスの状態が確定するまで待つ。また、await
はasync
関数の中でしか利用できない。
// Promiseを返す関数
function p() {
return new Promise((r) => {
console.log('start promise')
setTimeout(() => {
console.log('in setTimeout')
r(10)
}, 1000)
console.log('end promise')
})
}
// async 関数
async function a1(x) {
console.log('start a1')
// Promiseのインスタンスを指定してawait
const value = await p()
console.log('end a1')
return x * value;
}
// async 関数
async function a2() {
console.log('start a2')
// async関数の戻り値(Promiseのインスタンス)を指定してawait
const value = await a1(100)
console.log(value)
console.log('end a2')
}
a2()
簡単にいえば、async
、await
はnew Promise
のシンタックスシュガーのようなもので、いくつかの例外を除いて相互に書き換えができる。
function p() {
return new Promise((r) => {
console.log('start promise')
setTimeout(() => {
console.log('in setTimeout')
r(10)
}, 1000)
console.log('end promise')
})
}
function pa1(x) {
return new Promise((r) => {
console.log('start a1')
p().then((value) => {
console.log('end a1')
r(x * value)
})
})
}
function pa2() {
return new Promise((r) => {
console.log('start a2')
pa1(100).then((value) => {
console.log(value)
console.log('end a2')
})
})
}
pa2()
書き換えができないケースの例としては、上記のp関数のようにsetTimeoutのコールバック内でresolveされるようなケースがあり、こういったケースではPromiseで記述できてもasync/awaitで書き換えることはできない。