k-tokitoh

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: [...], ...
// }

何が起きているのだろうか?



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()のハンドラー関数を実行し始めることができている。

ここで何が起きているか。



これでどうやらうまくできたみたいです。

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", ...]