Help us understand the problem. What is going on with this article?

【async/await版】JavaScriptでループ中にスリープしたい。それも読みやすいコードで

JavaScriptでループ中にスリープしたい。それも読みやすいコードでという5年くらい前の記事を読み、コメント欄に、今だったらasync/awaitを使ってこうできるよーという例が挙げあられていたんですが、パッと見て理解できなかったのでメモとして残しておきます。

理解するのに役立ったのは、JavaScript Promiseの本の1.2.1. Promise workflow5.3. await式です。
setTimeoutの挙動をgifで説明している♻️ JavaScript Visualized: Event Loopも参考になりました。

今回は、1000ミリ秒ごとにランダムな数字を出力する処理を10回繰り返すコードを目標とします。

以下は、動かないけど実現したいコードです。

for (let i = 0; i < 10; i++) {
    console.log(Math.random())
    sleep(1000);
}

async/awaitでやってみる

上のコードは、以下のように async/await を用いて実現できます。

function sleep(milliseconds) {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

async function main() {
  for (let i = 0; i < 10; i++) {
    console.log(Math.random());
    await sleep(1000);
  }
}

main()

ちょっとした解説

  • await式では非同期処理を実行し完了するまで、次の行(次の文)を実行しない
    => sleep(1000)の実行が終わるまでfor文の次の周期にはいかない(1000ミリ秒停止する)。

  • sleep関数はPromiseを返している
    => sleep(1000)の実行が終わる = PromiseがFulfilledになる = promiseオブジェクトがsetTimeoutでmilliseconds(1000ミリ秒)後にresolveされる

new Promise(resolve => setTimeout(resolve, milliseconds)); ってなんだ?

違和感があったのは、await式の返り値がないことです。
例えば、以下のコードだとawait式の返り値は sccess となります。

function sleep(milliseconds) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve("success")
        }, milliseconds)
    });
}

async function main() {
  for (let i = 0; i < 10; i++) {
    console.log(Math.random());
    let value = await sleep(1000);
    console.log(value) // sccess
  }
}

main()

しかし今回は時間を止めるだけなのでPromiseは値をresolveする必要はないため、ちょっと違和感のあるコードになっていました。

function sleep(milliseconds) {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

async function main() {
  for (var i = 0; i < 10; i++) {
    console.log(Math.random());
    let value = await sleep(1000);
    console.log(value) // undefined
  }
}

main()

async/awaitを使わずPromiseだけでやってみる

特に意味はないですが勉強になりそうなので。

Promiseのthenを復習する

参考 とほほのPromise入門

100の2倍を求める非同期関数の使用例です。

function func1(data, callback) {
  setTimeout(() => {
    callback(data * 2);
  }, 1000);
}

function sample_callback() {
  func1(100, function(value) {
    console.log(value); //200
  });
}

sample_callback();

この時点でつまずきそうですが、関数そのものを引数にして渡してるだけです。
これをPromiseで書き換えると以下になります。

function func2(data) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve(data * 2);
    }, 1000);
  });
}

function sample_promise() {
  func2(100).then(data => {
    console.log(data); // 200
  });
}

sample_promise();

Promiseが1000ミリ秒後に resolve(data * 2) と解決されて then の onFulfilled に設定された関数に data * 2 という値を渡します。
awaitの代わりにthenを使っているイメージを持っているのですがあってるでしょうか???

promise chain

以下のコードでは、promise chain をつなげて処理をしています。
returnされてきた func2(data)に対してさらに then で処理することを繰り返しています。
このコードでは、一秒間隔で 200, 400, 800 が出力されます。

function func2(data) {
  return new Promise(function(resolve) {
    setTimeout(() => {
      resolve(data * 2);
    }, 1000);
  });
}

function sample_promise3() {
  func2(100)
    .then(data => {
      console.log(data);
      return func2(data); // 200
    })
    .then(data => {
      console.log(data);
      return func2(data); // 400
    })
    .then(data => {
      console.log(data); // 800
    });
}

sample_promise3();

本題

ここまで理解できたのでasync/awaitを使わずPromiseだけでやってみます。

let myPromise = main(1000);

for (let i = 0; i < 3; i++) {
  myPromise = myPromise.then(num => {
    console.log(num);
    return main(1000);
  });
}

function main(milliseconds) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve(Math.random());
    }, milliseconds);
  });
}

for文の中身が少しややこしくて、

for (var i = 0; i < 3; i++) {
  myPromise = myPromise.then(num => {
    console.log(num);
    return main(1000);
  });
}

は、以下と(ほぼ)同義です。

myPromise
  .then(num => {
    console.log(num);
    return main(1000);
  })
  .then(num => {
    console.log(num);
    return main(1000);
  })
  .then(num => {
    console.log(num);
  });

もっと分かりやすくすると、

for (var i = 0; i < 3; i++) {
  myPromise = myPromise.then(hoge)
}

は、以下と同義です。

myPromise
  .then(hoge)
  .then(hoge)
  .then(hoge);

まとめ

Promiseのみを使う

let myPromise = main(1000);

for (let i = 0; i < 3; i++) {
  myPromise = myPromise.then(num => {
    console.log(num);
    return main(1000);
  });
}

function main(milliseconds) {
  return new Promise(function(resolve) {
    setTimeout(() => {
      resolve(Math.random());
    }, milliseconds);
  });
}

async/awaitを使う

function sleep(milliseconds) {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

async function main() {
  for (let i = 0; i < 3; i++) {
    console.log(Math.random());
    await sleep(1000);
  }
}

main()

async/awaitの方がだいぶ直感的ですね!!
「Promiseのみを使う」のlet myPromise = main(1000);で最初にpending状態のPromiseを定義するのも違和感がありますね。

半日かけでじっくりやったので、Promiseおよびasync/awaitが分かった気になってます。
間違いがあればご指摘お願いします!!

wafuwafu13
メモ感覚で書いてます。
https://twitter.com/wafuwafu13_
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした