2020-02-17
Promise
みんな Promise さくっと理解してる気がするけどむずくないですか。
ようやくなんとなく見えたのでメモ。
Promise 以前
XHR を考える。
const api = (url) => {
const req = new XMLHttpRequest()
req.open("GET", url)
req.onload = () => {
json = JSON.parse(req.response)
console.log(json)
}
req.send()
}
取得した値を戻り値にすることはできないので、戻り値は undefined.
使ってみた結果が以下。(ログ出力されたオブジェクトは内容を抜粋して記載しています。以下同じ。)
api('https://swapi.co/api/people?search=R2-D2')
// <- undefined
// => {
// results: [
// {name: "R2-D2", height: "96", mass: "32", homeworld: "https://swapi.co/api/planets/8/", ...}
// ]
// }
このままだと取得後に取得した値を元にして処理する、ということができない。
api('https://swapi.co/api/people?search=R2-D2').results[0].height
// <- TypeError: Cannot read property 'results' of undefined
戻り値は undefined なのでそりゃそうです。
これを解決するためにコールバック関数という仕組みがあるが、それは今回は省略する。
Promise
以下のように Promise オブジェクト(を返す関数)を用意する。
const apip = (url) => {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest()
req.open("GET", url)
req.onload = () => {
json = JSON.parse(req.response)
resolve(json)
}
req.send()
})
}
すると、取得した時点で、取得した結果を元に処理を続けることができる。
取得した json の一部をログ出力することもできるし、
apip('https://swapi.co/api/people?search=R2-D2')
.then(json => console.log(json.results[0].homeworld))
// <- Promise {<pending>}
// => https://swapi.co/api/planets/8/
取得した API を叩くこともできる。
apip('https://swapi.co/api/people?search=R2-D2')
.then(json => api(json.results[0].homeworld))
// <- Promise {<pending>}
// => {
// name: "Naboo", diameter: "12120", resident: [...], ...
// }
何が起きているのだろうか?
- apip は、その中で Promise のコンストラクタを呼び出す。
- すると、コンストエストが送信される。
- 実行後、Promise オブジェクトが生成される。これを p1 と呼ぼう。
- Promise オブジェクトは、resolve()が実行されるまでは pending 状態、resolve()が実行されると resolved 状態となる。
- p1 は生成時点ではまだ resolve()が実行されていないので、pending 状態である。
- で、then()もまた別の Promise を生成する。p2 と呼ぼう。
- p2 は、ハンドラー関数の実行が終わって値を返したときに、ハンドラー関数の戻り値を引数として resolve()を呼び出す=resolved になる。
- (ただしハンドラー関数が Promise オブジェクトを返すときは異なる。これについては後述する。)
- ハンドラ関数が呼ばれるのはいつかというと、then()のレシーバである Promise オブジェクト(=p1)が resolve()を実行するとき。
- 前述のとおり、p1 はまだ pending なので、p2 のハンドラ関数も呼び出されていない。
- よって、p2 も生成されたときは pending 状態である。
- しばらくすると p1 で実行していたリクエストのレスポンスが返ってくる。
- onload で登録されていた関数が呼ばれ、resolve(json)が実行される。つまり次に登録された p2 のハンドラー関数が呼び出される。
- この時点で p1 が resolved になる。
- p2 のハンドラー関数が実行され終えると、p2 も resolved になる。
then()をチェーンする
1 つめの then()で登録したハンドラー関数の実行が終わった後で、さらに処理をチェーンさせたい場合がある。
例えば以下のように。
apip('https://swapi.co/api/people?search=R2-D2')
.then(json => json.results[0].homeworld)
.then(string => "R2-D2の故郷の情報は " + string)
.then(string => string + " から取得できます。")
.then(string => console.log(string))
// <- Promise {<pending>}
// => R2-D2の故郷の情報は https://swapi.co/api/planets/8/ から取得できます。
n 個めの then のハンドラー関数が実行された 直後の時点 で、n+1 個めの then のハンドラー関数の引数が決定できるのでうまくいく。
しかし例えば、1 つめの then のハンドラー関数で再度 API を叩き、その結果を出力したい場合はどうだろうか。
以下のコードはうまく動作しない。
apip('https://swapi.co/api/people?search=R2-D2')
.then(json => api(json.results[0].homeworld))
.then(json => console.log(json.name))
// <- Promise {<pending>}
// <- TypeError: Cannot read property 'name' of undefined
1 つめの then()のハンドラー関数が呼ばれたときに、その戻り値が undefined なので、2 つめの then のハンドラー関数が undefined を引数として直ちに実行されてしまうからだ。
この問題を回避し、ある then()のハンドラー関数が非同期的に実行完了するまで、次の then()のハンドラー関数の実行を待つ、という仕組みが存在する。
そのためには、ハンドラー関数の戻り値を Promise オブジェクトにすればよい。
チェーンされた then()のハンドラー関数の実行タイミングを制御する
apip('https://swapi.co/api/people?search=R2-D2')
.then(json => apip(json.results[0].homeworld))
.then(json => console.log(json.name))
// <- Promise {<pending>}
// => Naboo
このコード例では、1 つ目の then()のハンドラー関数に含まれる HTTP 通信が完了するのを待ってから、2 つ目の then()のハンドラー関数を実行し始めることができている。
ここで何が起きているか。
- 先程と同様、p1, p2, p3 と Promise オブジェクトが生成される。
- p2 のハンドラー関数が実行されるところまでは同じである。
- p2 のハンドラー関数は、先程とは異なり、Promise オブジェクトを返す。これを p2-0 と呼ぼう。
- (p2-0 は生成直後には pending 状態である。)
- p2 は、ハンドラー関数の戻り値が Promise オブジェクト(=p2-0)である場合、その状態及び resolve する値を、戻り値の Promise オブジェクト(=p2-0)に依存する。
- p2-0 はまだ pending 状態なので、p2 も pending 状態となる。
- よって p3 のハンドラ関数も呼ばれない。
- しばらくすると、p2-0 で送信していたリクエストのレスポンスが返ってくる。
- onload で登録していた resolve(json)が実行され、p2-0 は resolved になる。
- p2-0 に依存している p2 においても、json を引数として実行される。
これでどうやらうまくできたみたいです。
then が 3 つ以上の場合にも同様の方法でチェーンしていけるはず。
おまけ: Promise.all
力尽きたのでコード例のみ。
apip('https://swapi.co/api/people?search=R2-D2')
.then(json => apip(json.results[0].homeworld))
.then(json => {
return Promise.all(
json.residents.map(resident => apip(resident))
)
}).then(residents => {
console.log(residents.map(resident => resident.name))
})
// <- Promise {<pending>}
// => ["R2-D2", "Palpatine", "Jar Jar Binks", "Roos Tarpals", "Rugor Nass", ...]