JavaScript で待機する方法(sleep 処理)

はじめに

JavaScript は、他プログラム言語のような sleep/wait/delay 関数がありません。

ですが、 window.setTimeout() のタイマー関数を使用することで擬似的に sleep() 関数を実現することができます。ただし、 setTimeout() だけでは、コールバック地獄に陥るなどの問題を抱えていました。

ES2015 対応の Promise は、この問題を解決したかに見えましたが、呼び出しの小難しさが残りました。最終的にES2017対応の async/await がこの問題を解決しました。

実装例を以降に示します。

async/await(ES2017 対応)

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
//const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

async function main() {
  console.log('start');
  await sleep(1*1000);

  console.log(0);
  await sleep(1*1000);

  console.log(1);
  await sleep(1*1000);

  for (var i=2; i<5; i++) {
    console.log(i);
    await sleep(1*1000);
  }

  console.log('end');
};
main();

async/await 構文が使用できるのであれば、これで決まりです。

async function(非同期関数)内で await 演算子を使用して、Promise(スリープ処理)の完了待ちをすることで実現しています。

Generator(ES2015 対応)

function sleep(ms, generator) {
  setTimeout(() => generator.next(), ms);
}

var main = (function*() {
  console.log('start');
  yield sleep(1*1000, main);

  console.log(0);
  yield sleep(1*1000, main);

  console.log(1);
  yield sleep(1*1000, main);

  for (var i=2; i<5; i++) {
    console.log(i);
    yield sleep(1*1000, main);
  }

  console.log('end');
})();
main.next();

ジェネレータ関数を再帰的に呼び出すことで実現しています。コードはきれいですが、使用できるかどうかは、対応環境次第です。

Promise(ES2015 対応)

クロージャーによる変数の受け渡し例function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function main() {
  let promise = Promise.resolve();

  promise = promise.then(() => console.log('start'))
                   .then(() => sleep(1*1000))
                   .then(() => console.log('0'))
                   .then(() => sleep(1*1000))
                   .then(() => console.log('1'))
                   .then(() => sleep(1*1000));

  for (var k=2; k<5; k++) {
    // クロージャーを利用した変数の受け渡し
    promise = promise.then((function(i) {
      return () => console.log(i);
    })(k)).then(() => sleep(1*1000));
  }

  promise = promise.then(() => {
    console.log('end');
  });
};
main();
引数による変数の受け渡し例function sleep(ms, arg) {
  return new Promise(resolve => setTimeout(() => resolve(arg), ms));
}

function main() {
  let promise = Promise.resolve({count:0});

  promise = promise.then((arg) => { console.log('start', arg); return arg; })
                   .then((arg) => sleep(1*1000, arg))
                   .then((arg) => { console.log(arg.count++); return arg; })
                   .then((arg) => sleep(1*1000, arg))
                   .then((arg) => { console.log(arg.count++); return arg; })
                   .then((arg) => sleep(1*1000, arg));

  for (var k=2; k<5; k++) {
    promise = promise.then((arg) => { console.log(arg.count++); return arg; })
                     .then((arg) => sleep(1*1000, arg));
  }

  promise = promise.then((arg) => {
    console.log('end', arg);
    return arg;
  });
};
main();

Promise を利用した、メソッドチェーンによる処理です。 for ループなどの変数の受け渡しでクロージャーを利用した複雑な処理や引数の受け渡し処理が必要になることがあります。

Promise は、ES2015 (ES6) で追加された機能です。ですが、 IE などの古いブラウザでも polyfill による Promise 対応が可能です。

setTimeout

function sleep(ms, func) {
  setTimeout(func, ms);
}

function main() {
  console.log('start');
  sleep(1*1000, function() {
    console.log('0');

    sleep(1*1000, function() {
      console.log('1');

      sleep(1*1000, function() {
        console.log('2');

        sleep(1*1000, function() {
          console.log('3');

          sleep(1*1000, function() {
            console.log('4');

            sleep(1*1000, function() {
              console.log('end');
            });
          });
        });
      });
    });
  });
};
main();

世にいうコールバック地獄です。関数の入れ子構造が積み重なり、コードがどんどんと読みにくくなっていきます。

XMLHttpRequest - 非推奨

// n秒間待機する
function sleep(n) {
  var request = new XMLHttpRequest();
  request.open('GET', '/sleep.php?n='+n, false);
  request.send(null);
}

function main() {
  console.log('start');
  sleep(1);

  console.log(0);
  sleep(1);

  console.log(1);
  sleep(1);

  for (var i=2; i<5; i++) {
    console.log(i);
    sleep(1);
  }

  console.log('end');
};
main();
sleep.php<?php sleep($_GET['n']);

サーバ側の待機処理を利用した、同期処理です。ただし、通信環境次第では、待機時間は想定より長くなる可能性があります。また、サーバ側にリクエストが無為に集中する問題もあります。採用すべきではありません。

ループ - 非推奨

function sleep(ms) {
  var time = new Date().getTime() + ms;
  while (new Date().getTime() < time) {}
}

function main() {
  console.log('start');
  sleep(1*1000);

  console.log(0);
  sleep(1*1000);

  console.log(1);
  sleep(1*1000);

  for (var i=2; i<5; i++) {
    console.log(i);
    sleep(1*1000);
  }

  console.log('end');

};
main();

ループと時間計測を利用した待機処理です。この方法は、 CPU の処理能力を無為に消費するため、採用すべきではありません。

補足

参考