コールバック関数と非同期処理

以前もコールバック関数に関するasync/ awaitの記事は書いたんだけど、やっぱ理解が甘かった。
同僚のインド人が丁寧に教えてくれて、かつドリル的なものをその時に作ってやってみたら解説してくれたからそれをまとめておく。

以前の記事はこちら。
https://www.sunapro.com/async-await%e3%81%a7%e3%81%ae%e9%9d%9e%e5%90%8c%e6%9c%9f%e5%87%a6%e7%90%86%e3%81%ae%e6%9b%b8%e3%81%8d%e6%96%b9/

目次

設定

arrayに[1, 2, 3]を格納しておく。
それをmapメソッドを使って全て2倍にし、newArrayに代入する。
その後、このnewArrayを出力する。
結果はもちろん[2, 4, 6]となる。

const array = [1, 2, 3];

const newArray = array.map(num => {
  return num * 2;
})

console.log(newArray);

//出力結果
[2, 4, 6]

ここで、仮にこのmapメソッドの中身が処理に時間がかかる場合どうなるかを見てみる。
mapメソッドの中でsetTimeoutを使って、わざと処理を1秒遅れるようにしてみたのが以下のコード。

const array = [1, 2, 3];

const newArray = array.map(num => {
  setTimeout(() => {
    console.log(num);
    return num * 2;
  }, 1000)
})

console.log(newArray);

//出力結果
[ undefined, undefined, undefined ]
//1秒後に以下が出力される
1
2
3

ハイライトした部分が増えた記述。
さて、出力がどうなったかと言うと、newArrayが全てundefinedとして出力されてからsetTimeoutの中の出力が1秒後にされている。

こうなった理由は、プログラムが上から順番に実行されて、array.mapの中のsetTimeoutで処理が待機中の間にconsole.log(newArray)が実行されてしまったからである。
当然この時は1秒処理待機中なので、return num * 2は実行されておらず、newArrayは定義されていないというカラクリ。

今回の目的

今回の目的は、setTimeoutの中身の処理を待った後でnewArrayを出力したい。
つまり実行する順序としては以下のようにしたい。

const array = [1, 2, 3];

const newArray = array.map(num => {
  setTimeout(() => {
    //実行①
    console.log(num);
    return num * 2;
  }, 1000)
})

//実行②
console.log(newArray);

したがって、出力結果が以下のようになればよい。

//プログラムを実行して1秒後の出力結果
1
2
3
[ 2, 4, 6 ]

実際の実装

失敗例 1

mapメソッドの部分の処理を待ってからconsole.log(newArray)を実行すればいいわけだから、async/ await使えばできそうだなと思ったので以下のように実装した。

const array = [1, 2, 3];

async function test() {
  const newArray = await array.map(num => {
    setTimeout(() => {
      console.log(num);
      return num * 2;
    }, 1000);
  })

  console.log(newArray)
}

test();

//出力結果
[ undefined, undefined, undefined ]
//1秒後に以下が出力される
1
2
3

まず、async/ awaitを使いたかったので、全体をasyncの関数でラップした。
そして、array.mapの処理をawaitにした。
しかし出力結果は変わらず。。

そうだった。。
mapメソッドの中身はコールバック関数で、この中の処理まではawaitしてくれないということを前回学んだのだった。
https://www.sunapro.com/2020/06/14/2020-06-14-110721/

失敗例 2

前回の学びを活かして、コールバック関数の中身もちゃんとawaitした。

const array = [1, 2, 3];

async function test() {
  const newArray = await array.map(async num => {
    await setTimeout(() => {
      console.log(num);
      return num * 2;
    }, 1000);
  })

  console.log(newArray)
}

test();

//出力結果
[ Promise { undefined }, Promise { undefined }, Promise { undefined } ]
//1秒後に以下が出力される
1
2
3

undefinedがPromise { undefined }に変わったが、期待通りの結果にはならず。。

これも結局setTimeoutの実行まではawaitしてくれるが、その関数の中身の1秒後の処理までは待ってくれないということらしい。
なのでsetTimeoutの部分がPromiseオブジェクトとして返ってくるので上記のような結果になったと解釈している。

ならばと思ってsetTimeoutのところにもasync/ awaitにしたが結果は同じだった。

そもそもの書き方から考え直さなきゃいけないらしい。
setTimeoutの中の処理が終わるまで待ってくれるような処理を書かないといけない。

成功例

うまくいったコードは下記。

const array = [1, 2, 3];

async function test() {
  const newArray = await Promise.all(array.map(num => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log(num);
        resolve(num * 2);
      }, 1000);
    })
  }))

  console.log(newArray)
}

test();

//出力結果
//1秒後に以下が出力される
1
2
3
[ 2, 4, 6 ]

期待する動きになった。

大枠の考え方としては以下の通り。

  • mapメソッドの返り値をreturn new Promiseとすることで、このPromiseが終わって初めてreturnされるようにする
  • mapメソッドの処理がPromiseオブジェクトになるので、それらが全て終わるまで待つようにawait Promise.allにする

なるほど、納得。
複雑に見えるけど一つずつ整理したら理解できるな。

おまけ

じゃあこうしたら出力はどうなると思う?とお題を出してもらった。
処理は先ほどと変わってないが、await Promise.allする前の部分の処理をpromiseArrayとして切り分けて、その部分を先にconsole.logで見てみたらどうなるのかという話。

const array = [1, 2, 3];

async function test() {
  const promiseArray = array.map(num => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log(num);
        resolve(num * 2);
      }, 1000);
    })
  })

  console.log("promise", promiseArray);

  const newArray = await Promise.all(promiseArray)

  console.log("new", newArray)
}

test();

処理を順番に追っていくと、
promiseArrayが処理されてreturnでnew Promiseを待っている間にconsole.log("promise", promiseArray)の部分が実行されるから、先ほどと同じでPromise { undefined }が最初に出力されて後は同じになるのでは?と思った。

しかし結果は少し違った。

//出力結果
promise [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
//1秒後に以下が出力される
1
2
3
new [ 2, 4, 6 ]

なんとPromise { undefined }ではなく、Promise { <pending> }が出力された。

はじめまして。

先ほどのPromise { undefined }の時はPromiseオブジェクトが作られてすぐにreturnされていたので、Promiseの処理が何もされていないためundefinedになったと認識している。

const newArray = await array.map(async num => {
  await setTimeout(() => {
    ...
  })
}

それに対して今回のはreturn new Promiseとしているので処理中のPromiseオブジェクトがreturnされたためにPromise { <pending> }になったと認識している。

const promiseArray = array.map(num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      ...
    })
  })
})

言語化してみたけど合ってるかはあまり自信がないな。。

まとめ

async/ await使えば可読性上がるし、new Promiseとか使う必要あるのかなと思ってたけど、こういう場面では使わなきゃいけないのかな?
await使ったいい感じのやり方はあるのかもしれないが、調べてもよく分からなかった。

今回はこれで納得できたからよしとする。

非同期に関する記事は色々見たけど、結局自分でこういうの作って手を動かすのが一番理解できる。
特にnew Promiseについては初めて使ったのでとても勉強になった。

参考

Promise と async/await の理解度をもう1段階上げる
https://qiita.com/SotaSuzuki/items/f23199e864cba47455ce

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次