タイマー処理同期・非同期

【JavaScript】一定時間停止(sleep/wait)の実装方法と仕組み

更新日:2024/02/27

JavaScriptでsleepやwait機能を実現させるためのコードを紹介します。
基本的にコピペで実装できますが、仕組みを知っておくとJavaScriptの理解を深めることができます。
まずはコード例から紹介しているので、お急ぎの型はコピペで実装してください。
理解を深めたい方は、少し詳しく解説しているので、最後まで読んでみてください。


2023/10 内容を整理しました

 

sleep/waitとは

他のプログラム言語でsleepやwaitと呼ばれている機能は、プログラムコードの処理を中断して、一定の時間が経過後に再開する機能です。
JavaScriptには、このような機能が用意されていません。

ですが、JavaScriptの機能を利用することで疑似的にsleepやwaitを実現できます。
※sleepとwaitは同じ機能のため、今後はsleepで統一します。

 

sleepのコード例

sleep機能の実装方法はいくつかありますが、1回だけsleepするならsetTimeout、複数回sleepするならasync/awaitをお勧めします。
まずは、コピペで使ってみてください。

setTimeoutでのsleep

setTimeoutは指定時間(ミリ秒)経過後に、コールバック関数を呼び出します。

// なんらかの処理
setTimeout( ()=>{
        // 1秒後の処理
    }, 1000);

単なるタイマー処理なので、一時停止では無いと感じると思います。
しかしJavaScriptは仕組み上、コードを一時停止できません。
そのため、タイマー処理で疑似的な一時停止をおこなうしかないのです。

sleep関数(1行版)

前項のsetTimeoutでのsleepで一時停止を順番に複数回おこなおうとすると、コールバック関数が入れ子になりとても分かりにくいコードになる可能性があります。

そこで、一時停止を複数回おこなうときは、次のようなPromiseを使用したsleep関数を使用します。

  // sleep関数 waitTime:停止する時間(ミリ秒)
const sleep = waitTime => new Promise( resolve => setTimeout(resolve, waitTime) );

この関数は、引数で停止時間(ミリ秒)を受けとります。
関数内部では、setTimeout()を実行してPromiseを返します

アロー関数を使用することで、1行で無理なくわかりやすいコードになっています。

Promiseについては、次のページを参考にしてください。
【JavaScript】 非同期はPromise?解説が難しいので自分で理解してみた

sleep関数で1秒待つ

sleep関数を使って、1秒間停止するコード例です。
async関数内で、awaitを付けて実行します。

const aFunc = async ( )=>{ // 必ずasync関数内で使用
    // なんらかの処理
    await sleep( 1000 ); // 1秒停止
     // 1秒停止後の処理(1回目)
    await sleep( 1000 ); // 1秒停止
     // 1秒停止後の処理(2回目)
}
aFunc(); // aFuncを実行

一時停止が1回だけのときはsetTimeoutと書きましたが、async関数化が面倒でないなら上記の方法でも問題ありません。

async/awaitについては、次のページを参考にしてください。
【JavaScript】 async/awaitを解説します

ある条件を満たすと、async関数外でawaitを使用できます。
次のページを参考にしてください。
【JavaScript】トップレベルawaitとは?トップレベルawaitが使えない理由と対処法

イベントリスナーでの一時停止

ブラウザのボタンを押されたときに呼び出さラるイベントリスナーなどで、一時停止するときは次のように記述します。

document.getElementById("btn").addEventListener("click"
      ,async ()=>{
            // なんらかの処理
            await sleep( 1000 ); // 1秒停止
            // 1秒停止後の処理
       }
   );

ここではイベントリスナーに無名関数を記述しています。
次のように、関数定義したものを記述することもできます。

document.getElementById("btn").addEventListener("click", aFunc );

Node.jsの場合

Node.jsのv15.0.0以降は、次のようなコードでsleepを実装できます。

一定時間停止するsleep関数:Node.js

// promisesを返すsetTimeoutを読み込む
const {setTimeout} = require("timers/promises");
// または、import { setTimeout } from "timers/promises";

const sleep = waitTime => setTimeout( waitTime );

 

sleepの仕組み

sleepの仕組みを知るには、まずはJavaScriptのイベントループについて知っておく必要があります。

ブラウザ上でボタンが押されたり、ファイルの読み込みが終了するなどJavaScriptで処理をおこなう必要がある場合、まずはイベントキューにイベントとして登録されます。

イベントキューはイベントループが常時監視していて、イベントを順番に処理しています。
イベントの中身にはJavaScriptのプログラムコードが含まれています。
ボタンクリックなら、addEventListenerで登録した関数が実行されます。

イベントループとイベントキュー

sleep関数の流れ

では、sleep関数の流れを見ていきます。

  // sleep関数 waitTime:停止する時間(ミリ秒)
const sleep = waitTime => new Promise( resolve => setTimeout(resolve, waitTime) );

まずはsetTimeoutでタイマー登録され、待ち時間が経過するとタイマーイベントがイベントキューに登録されます。

イベントループとタイマーイベント

タイマーイベントは他のイベントが終わった後に処理されます。
他のイベントの処理時間が長ければ長いほど、setTimeoutのコールバック関数の呼び出しが遅くなるのです。

イベントループの処理方法は処理系によって異なります。
Node.jsはイベント処理後にタイマーチェックをおこなうことでタイマーの遅延を最小限に抑えています。
しかし、直前のイベント処理が終わるまで処理できないのは変わりがないため、遅延がでます。

setTimeoutのコールバック関数はresolveです。
これは、イベントキューにPromiseイベントを登録する関数です。

resolveによりPromiseイベントがイベントキューに登録される

setTimeoutのコールバック関数終了後にイベントループに戻り、他の優先イベント処理後にPromiseイベントが実行され、thenなどで登録した関数が呼び出されます。

async/awaitの仕組み

Promiseイベントでthenなどで登録した関数が呼び出されるのですが、async/await内でthenを使用していません。

const aFunc = async ( )=>{ // 必ずasync関数内で使用
    // なんらかの処理
    await sleep( 1000 ); // 1秒停止
     // 1秒停止後の処理(1回目)
    await sleep( 1000 ); // 1秒停止
     // 1秒停止後の処理(2回目)
}
aFunc(); // aFuncを実行

実際には、内部的にpromiseのthenとして処理されるように組み替えられます。

const aFunc = ( )=>{
    // なんらかの処理
    sleep( 1000 ).then( ()=>{
       // 1秒停止後の処理(1回目)
       return sleep( 1000 );
    } ).then( ()=>{
      // 1秒停止後の処理(2回目)
    });
}
aFunc(); // aFuncを実行

実際に書き換えているわけではありませんが、流れとしては上記のコードにようになります。

sleepの注意点

async/awaitはpromise…thenに組み替えられるため、呼び出し元のコードがあれば実行されます。

console.log( "aFunc実行" );
aFunc();
console.log( "aFunc終了" ); // sleep中のはずが、実行されてしまう。

この場合は、次のようにasync関数内でawait呼び出しするように変更します。

const bFunc = async ()=>{
   console.log( "aFunc実行" );
   await aFunc();
   console.log( "aFunc終了" ); // sleep中のはずが、実行されてしまう。
}
bFunc(); 

またsleep中でも、ブラウザのボタンクリックやファイル読み込み通知などを受け取ります。

sleep中に処理が重複のを避けたいときは、フラグ等で管理してください。

■フラグ管理例


let isRun=false;
const asyncFunc = async ( )=>{
    if( isRun ) return; // 処理が実行中
    isRun = true;
    // なんらかの処理
    await sleep( 3000 );
    // なんらかの処理
    isRun = false;
}

document.getElementById("btn").addEventListener("click",asyncFunc);

 

async/awaitは少しだけ非効率

気にする必要は全くありませんが、Promiseおよびasync/await版のsleep関数は少しだけ非効率です。

async/await版のsleep関数は、イベントループ ⇒ コールバック関数(Timer) ⇒ イベントループ ⇒ 後処理(Promise) の流れです。

setTimeoutのみなら、イベントループ ⇒ コールバック関数(Timer) の流れです。

async/awaitは少し非効率ですね。

実際にはタイムラグは微々たるものなので、気にする必要はありません。

 

その他のSleepコード例

Promiseを使用したsleep関数コード

sleep関数をasync/awaitを使用せずに実行する例です。

  // sleep関数 waitTime:停止する時間(ミリ秒)
const sleep = waitTime => new Promise( resolve => setTimeout(resolve, waitTime) );
  // sleep関数使用例
sleep(3000) // 3秒停止
  .then( ()=>{ 
        // 3秒停止後の処理
      return sleep(1000); // 1秒停止
  }).then( ()=>{
        // 1秒停止後の処理
  });

メリット:停止後の処理をthen()で重ねていくだけなのでわかりやすい。
デメリット:一時停止関数のイメージからズレている。

setTimeout入れ子回避バージョン

setTimeoutを入れ子にすると、とても分かりにくいコードになります。
次のコードは、入れ子にせずにsetTimeoutを順番に複数回呼び出しています。

const sleepFuncs = [
    { waitTime: 3000 , func : ()=>{ /* 3秒停止後の処理 */ } },
    { waitTime: 1000 , func : ()=>{/* 1秒停止後の処理 */ } }
  ];
const sleep = funcData =>{
  const length = funcData.length;
  let count = 0;

  const waiter = waitTime => setTimeout( ()=>{
    funcData[count++].func();
    if( count < length ) waiter( funcData[count].waitTime );
  },waitTime);

  waiter( funcData[0].waitTime );
}

sleep( sleepFuncs );

メリット:イベント呼び出しが1回のみなので効率が良い。
デメリット:ここまでしてやる意味があまりない。

setIntervalを使用したsleep関数コード

JavaScriptのタイマー処理にはsetTimeoutの他に、setIntervalがあります。
次のコードはsetIntervalを使用して、一定時間の停止を一定回数繰り返しています。

const sleepRepeat = ( waitTime , repeatTimes , func ) =>{
  let count = 0;
  const id = setInterval( ()=>{
    func(++count);
    if( count >= repeatTimes ) clearInterval(id);
  } , waitTime );
}

sleepRepeat( 3000 , 5 , (count)=>{
  console.log( `${count}回目` );
});

メリット:同じ間隔で同じ処理を行うのに向いている
デメリット:本来のsleep処理には向いていない

ループを使用したsleep関数コード

何が何でも一時停止中に他の処理が割り込んでほしくない。
そんなときは、ループで時間経過を待ちます。

const sleep = (waitTime)=>{
  const startTime = Date.now();
  while( Date.now() - startTime < waitTime );
};
    // なんらかの処理

sleep( 3000 ); // 3秒停止

     // 3秒停止後の処理

sleep( 1000 ); // 1秒停止

     // 1秒停止後の処理

メリット:waitやasyncを使用しなくていい。
デメリット:ブラウザ表示やクリックなどを妨げ、システムが停止したような印象を与える可能性がある。

更新日:2024/02/27

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。