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

【Node.js】 PromiseベースのタイマーsetTimeoutとsetInterval

更新日:2021/08/26

JavaScriptにはタイマー監視に関するsetTimeoutとsetIntervalメソッドが用意されています。
これらはコールバック関数により通知されます。

Node.jsにもこの機能がありますが、Promise機能を使用したタイマー監視も用意されていて、見通しの良いコード作成に役立てることができます。

ここでは、Promise機能を使用したタイマーについてお伝えします。

 

setTimeoutとsetInterval

setTimeoutとsetIntervalは、JavaScriptでタイマー監視をおこなうために、監視時間とコールバック関数を登録するメソッドです。

詳細は別の記事で解説しているので、そちらをご覧ください。

【JavaScript】 タイマー処理/setTimeoutとsetInterval

なお次項で紹介するPromise版のsetTimeoutとsetIntervalは、通常版のsetTimeoutとsetIntervalと同様にタイマー遅延が発生します。

理由は、上記リンク先をご覧ください。

 

Promise版タイマーメソッドの読み込み

Node.jsは通常のsetTimeoutとsetIntervalメソッドの他に、Promiseを返すsetTimeoutとsetIntervalメソッドが用意されています。

Promise版タイマーメソッドはv15.0.0より導入されました。それ以前のバージョンでは使用できません。

これらのメソッドを使用することで、見通しの良いコードを作成することができます。

Promise版のsetTimeoutとsetIntervalメソッドは、timers/promisesモジュールに含まれています。
プログラムで使用するためには、importで読み込む必要があります。

Promise版タイマーメソッドのimport読み込み


import { setTimeout , setInterval } from "timers/promises";

importでエラーが出るときは、requireを使用します。

Promise版タイマーメソッドのrequire読み込み


const { setTimeout , setInterval } = require("timers/promises");

 

Promise版setTimeout/setInterval

Promise版setTimeoutとsetIntervalは、通常のsetTimeoutとsetIntervalと引数が異なります。

構文

Promise版setTimeout/setIntervalの構文

setTimeout( 指定時間 , 値 , オプション )
setInterval( 指定時間 , 値 , オプション )

引数

■指定時間

省略可能です。省略時の規定値は1です。
指定時間はミリ秒で指定します。

指定時間以上の時間が経過すると、Node.jsのシステムがsetTimeoutで返されたPromiseオブジェクトを解決させます。

■値

省略可能です。
Promiseオブジェクトの解決結果として引き渡される値です。

通常版のsetTimeoutはコールバック関数に渡す引数を指定できますが、同じような意味合いで使用できます。

■オプション

省略可能です。
使用方法は後述します。

戻り値

setTimeoutとsetIntervalは戻り値が異なります。

■setTimeout

Promiseオブジェクトを返します。
thenやcatchメソッド、async/awaitキーワードなどが使用できます。

■setInterval

非同期イテレーターを返します。
for-await-of構文などで処理をする必要があります。

 

Promise版setTimeoutの使用方法

Promise版setTimeoutの使用方法を紹介します。

thenでの処理

次のコードは3秒以上経過を待ち、実際の経過時間をコンソールに出力する処理を3回おこなっています。

3秒以上経過を待つ


const { setTimeout } = require("timers/promises");

const thenFunc = value =>{
    console.log( Date.now() - value.startTime + "ミリ秒経過" );
    return setTimeout( value.wait , value );
};

const timerData = { wait : 3000 ,  startTime : Date.now() };

setTimeout( timerData.wait , timerData )
    .then( thenFunc )
    .then( thenFunc )
    .then( thenFunc );

// 結果:
// 3008ミリ秒経過
// 6031ミリ秒経過
// 9038ミリ秒経過

このコードは問題点が一つあります。
3回目のthenで呼び出される関数内でsetTimeoutを実行している点です。

本来なら3回目の出力でNode.jsの処理が終わるべきですが、タイマーを登録しているため3秒以上経過後に終了します。
最後だけ処理を変えるなどの工夫が必要です。

async/awaitを使用

async/awaitを使用することで、見かけ上一時停止するコードを作成できます。

見かけ上一時停止するコード


const { setTimeout } = require("timers/promises");

const sleep = waitTime => setTimeout( waitTime ) ;

const aFunc = async function( ){

    // なんらかの処理

    await sleep( 3000 ); // 3秒(以上)停止

    // なんらかの処理

    await sleep( 1000 ) ;// 1秒(以上)停止

    // なんらかの処理

}
aFunc();

sleepについては次の記事で紹介しています。
【JavaScript】 一定時間停止(sleep)のやりかた

 

Promise版setIntervalの使用方法

for-await-of構文

Promise版setIntervalの戻り値は、非同期イテレーターです。
非同期イテレーターは、for-await-of構文で順番に値を処理していきます。


const {setInterval} = require("timers/promises");

(async ()=>{
    for await ( const startTime of setInterval( 3000 , Date.now() ) ){
        console.log( Date.now() - startTime + "ミリ秒経過" );
    }
})();

for-of構文(動作しない)

for-of構文のステートメント内でawaitを使用する方法も考えられます。


const {setInterval} = require("timers/promises");

(async ()=>{
    for  ( const timerPromise of setInterval( 3000 , Date.now() ) ){
        const startTime = await timerPromise;
        console.log( Date.now() - startTime + "ミリ秒経過" );
    }
})();

setIntervalの結果からうまく値が取り出せないためか、ステートメント内部に処理が移りません。

for-of構文は非同期イテレーターに対応していないため、そもそも使用できません。
次のように、async関数の外で実行すると、エラーが発生します。


for  ( const timerPromise of setInterval( 3000 , Date.now()  ) ){
    console.log( timerPromise );
}
// TypeError: setInterval is not a function or its return value is not iterable

async関数内部で使用するとエラーが表示されず、さらにステートメントに処理が移らないというとても困ったことになります。
setIntervalでfor-ofは使用しないようにしましょう。

nextメソッドを呼び出す

タイマーを個別にawaitしたいときは、非同期イテレーターのnextメソッドを呼び出します。


(async ()=>{

    const si = setInterval( 3000 , Date.now() );

    let result = await si.next();

    while( !result.done ){
        console.log( Date.now() - result.value + "ミリ秒経過" );
        result = await si.next();
    }
})();

 

3番目の引数について

Promise版setTimeoutとsetIntervalは、3番目の引数としてオプション値を指定できます。

オプション値を次のオブジェクトで指定します。

オプション値の形式

{
ref : ブール型
signal : AbortSignalオブジェクト
}

オプションref:タイマー監視の動作

オプションrefは、イベントループ中に他のイベントが完了してタイマー監視のみ残っているとき、どのような動作をするかを指定します。

true: タイマー監視を続けます
false: タイマー監視を中止し、プロセスを終了します。

規定値はtrueです。

次のコードは、タイマーの監視をせずに終了します。


const {setTimeoutl} = require("timers/promises");

setTimeout(3000,null,{ref:false})
    .then( ()=>console.log( "3秒経過" ) );

オプションsignal:タイマーのキャンセル

オプションsignalは、タイマー監視をキャンセルするために必要なAbortSignalを指定します。

詳しくは、次項で解説します。

 

Promise版タイマーのキャンセル

Promise版タイマーをキャンセルする必要があるときは、3番目の引数にAbortSignalを指定します。

次の手順で処理をします。

(1) new AbortController() で アボートコントローラーを作成する。
(2) タイマーの第三引数にオブジェクト{ signal: アボートコントローラー.signal } を指定する
(3) キャンセルしたいタイミングで、アボートコントローラー.abort() を実行する

キャンセルをおこなうと、エラーが発生します。
catch句で補足してください。

Promise版setTimeoutのキャンセル

次のコードは3秒と5秒のタイマーを用意して、3秒経過後に5秒のタイマーをキャンセルしています。


const { setTimeout } = require("timers/promises");

const abortController = new AbortController();

  // 3秒タイマー
setTimeout(3000,"3秒経過")
    .then( value=>{
        console.log( value );
        abortController.abort(); // 5秒タイマーをキャンセル
    } );

  // 5秒タイマー
setTimeout(5000 , "5秒経過", { signal: abortController.signal })
    .then( console.log )
    .catch( () => {
        console.log( 
             abortController.signal.aborted 
                  ? "中断されました" : "不明のエラー"
         );
    } );

最後のcatch()を忘れると、次のエラーが発生します。

AbortError: The operation was aborted

キャンセルを行った場合、abortController.signal.aborted がtrueになります。
キャンセル以外でcatch句が捕捉される可能性がある場合は、この値を確認してください。

Promise版setIntervalのキャンセル

setIntervalのキャンセルは次の二つのケースが考えられます。

(1) タイマー経過後の処理を行った後、次のタイマー待ちをキャンセルする

(2) 現在のタイマー待ちをキャンセルする。

この二つについて、順番に解説していきます。

次のタイマー待ちをキャンセルする

for-await-ofでの繰り返し処理は、breakで終了することができます。

次のコードは、3秒間隔で処理を繰り返しています。
処理後、総時間が10秒を超えているかをチェックし、超えていたらfor-await-ofを抜けます。

for-await-ofの中断


const { setInterval } = require("timers/promises");

(async ()=>{
    for await ( let startTime of setInterval( 3000 , Date.now() ) ){
        const time = Date.now() - startTime;
        console.log( time + "ミリ秒経過" );

        if( time > 10000 ) break; // for-await-ofを抜ける
    }
    console.log( "タイマーを終了しました" );
})();

現在のタイマー待ちをキャンセル

タイマー監視中にキャンセルするときは、setTimeoutのケースと同じように、3番目の引数にAbortSignalを指定します。

次のコードは、10秒経過した時点でsetIntervalでのタイマー監視をキャンセルしています。

現在のタイマー待ちをキャンセル


const { setTimeout , setInterval } = require( "timers/promises");

const abortController = new AbortController();

// キャンセルをおこなう10秒タイマー
setTimeout(10000,"10秒経過")
    .then( value =>{
        console.log( value + "→アボートします" );
        abortController.abort();
    } );

(async ()=>{
    try {
        for await ( let startTime of setInterval( 3000 , Date.now() , { signal: abortController.signal }) ){

            console.log( Date.now() - startTime + "ミリ秒経過" );

        }
    }catch( e ){
        console.log(
            abortController.signal.aborted
                ? "中断されました" : "不明のエラー"
        );
    }
})();

ポイントは、abortController.abort()を実行するとエラーが発生するので、try-catchを使用している点です。

try-catchを使用したくないときは、Promiseのcatchでエラーを捕捉できるように、nextメソッドを呼び出して手動でループします。


(async ()=>{

    const si = setInterval( 3000 , Date.now(), { signal: abortController.signal });

    let result = await si.next().catch(e=>({ done:true }) );

    while( !result.done ){
        console.log( Date.now() - result.value + "ミリ秒経過" );
        result = await si.next().catch(e=>({ done:true }) );
    }
    console.log(
        abortController.signal.aborted
            ? "中断されました" : "不明のエラー"
    );

})();

更新日:2021/08/26

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

スポンサーリンク

記事の内容について

null

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

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

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

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

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

 

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