【Node.js】 PromiseベースのタイマーsetTimeoutとsetInterval
更新日:2021/08/26
JavaScriptにはタイマー監視に関するsetTimeoutとsetIntervalメソッドが用意されています。
これらはコールバック関数により通知されます。
Node.jsにもこの機能がありますが、Promise機能を使用したタイマー監視も用意されていて、見通しの良いコード作成に役立てることができます。
ここでは、Promise機能を使用したタイマーについてお伝えします。
- 1setTimeoutとsetInterval
- 2Promise版タイマーメソッドの読み込み
- 3Promise版setTimeout/setInterval
- 構文
- 引数
- 戻り値
- 4Promise版setTimeoutの使用方法
- thenでの処理
- async/awaitを使用
- 5Promise版setIntervalの使用方法
- for-await-of構文
- for-of構文(動作しない)
- nextメソッドを呼び出す
- 63番目の引数について
- オプションref:タイマー監視の動作
- オプションsignal:タイマーのキャンセル
- 7Promise版タイマーのキャンセル
- Promise版setTimeoutのキャンセル
- Promise版setIntervalのキャンセル
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構文などで処理をする必要があります。
非同期イテレーターについては次のページを参考にしてみてください。
■【JavaScript】 非同期イテレータと非同期ジェネレーターについて理解しよう
■【JavaScript】 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
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。