関数を今すぐ実行するのではなく、後で特定の時間に実行するように設定することができます。これを「呼び出しのスケジューリング」といいます。
そのためには2つのメソッドがあります。
setTimeoutを使用すると、時間間隔の後に関数を一度だけ実行できます。setIntervalを使用すると、時間間隔の後に関数を繰り返し実行し、その後その間隔で継続的に繰り返し実行できます。
これらのメソッドはJavaScript仕様の一部ではありません。しかし、ほとんどの環境には内部スケジューラがあり、これらのメソッドを提供しています。特に、すべてのブラウザとNode.jsでサポートされています。
setTimeout
構文
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
パラメータ
func|code- 実行する関数またはコードの文字列。通常は関数です。歴史的な理由から、コードの文字列を渡すこともできますが、推奨されません。
delay- 実行するまでの遅延(ミリ秒単位、1000ms = 1秒)、デフォルトは0。
arg1,arg2…- 関数の引数
例えば、このコードは1秒後にsayHi()を呼び出します。
function sayHi() {
alert('Hello');
}
setTimeout(sayHi, 1000);
引数付き
function sayHi(phrase, who) {
alert( phrase + ', ' + who );
}
setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John
最初の引数が文字列の場合、JavaScriptはその文字列から関数を作成します。
したがって、これも機能します。
setTimeout("alert('Hello')", 1000);
しかし、文字列の使用は推奨されません。代わりに、このようなアロー関数を使用してください。
setTimeout(() => alert('Hello'), 1000);
初心者開発者は、関数に括弧()を追加するという間違いを犯すことがあります。
// wrong!
setTimeout(sayHi(), 1000);
これは機能しません。setTimeoutは関数の参照を期待しているためです。ここでsayHi()は関数を実行し、その実行結果がsetTimeoutに渡されます。私たちのケースではsayHi()の結果はundefined(関数は何も返さない)なので、何もスケジュールされません。
clearTimeout でキャンセル
setTimeoutへの呼び出しは、「タイマー識別子」timerIdを返し、これを使用して実行をキャンセルできます。
キャンセルする構文
let timerId = setTimeout(...);
clearTimeout(timerId);
以下のコードでは、関数をスケジュールしてからキャンセルします(考えを変えました)。その結果、何も起こりません。
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // timer identifier
clearTimeout(timerId);
alert(timerId); // same identifier (doesn't become null after canceling)
alertの出力からわかるように、ブラウザではタイマー識別子は数値です。他の環境では、これは別のものになる可能性があります。例えば、Node.jsは追加のメソッドを持つタイマーオブジェクトを返します。
繰り返しになりますが、これらのメソッドに関する普遍的な仕様はないため、問題ありません。
ブラウザの場合、タイマーはHTML Living Standardのタイマーセクションで説明されています。
setInterval
setIntervalメソッドは、setTimeoutと同じ構文です。
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
すべての引数は同じ意味を持ちます。しかし、setTimeoutとは異なり、関数を一度だけ実行するのではなく、指定された時間間隔後に定期的に実行します。
以降の呼び出しを停止するには、clearInterval(timerId)を呼び出す必要があります。
次の例では、2秒ごとにメッセージが表示されます。5秒後、出力は停止します。
// repeat with the interval of 2 seconds
let timerId = setInterval(() => alert('tick'), 2000);
// after 5 seconds stop
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
alertが表示されている間も時間は経過します。ChromeやFirefoxを含むほとんどのブラウザでは、内部タイマーはalert/confirm/promptを表示している間も「動作し続けます」。
そのため、上記のコードを実行し、しばらくの間alertウィンドウを閉じない場合、alertは閉じるとすぐに次のalertが表示されます。アラート間の実際の間隔は2秒より短くなります。
ネストされたsetTimeout
何かを定期的に実行するには2つの方法があります。
1つはsetIntervalです。もう1つは、次のようなネストされたsetTimeoutです。
/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
上記のsetTimeoutは、現在のもの(*)の最後で次の呼び出しをスケジュールします。
ネストされたsetTimeoutは、setIntervalよりも柔軟性の高いメソッドです。このようにして、次の呼び出しは、現在の呼び出しの結果に応じて異なる方法でスケジュールできます。
例えば、データ要求のために5秒ごとにサーバにリクエストを送信するサービスを作成する必要がありますが、サーバが過負荷になっている場合は、間隔を10秒、20秒、40秒…に増やす必要があります。
擬似コードを以下に示します。
let delay = 5000;
let timerId = setTimeout(function request() {
...send request...
if (request failed due to server overload) {
// increase the interval to the next run
delay *= 2;
}
timerId = setTimeout(request, delay);
}, delay);
そして、スケジュールしている関数がCPU負荷の高いものの場合、実行にかかった時間を測定し、次の呼び出しを早くするか遅くするかを計画することができます。
ネストされたsetTimeoutを使用すると、setIntervalよりも正確に実行間の遅延を設定できます。
2つのコードフラグメントを比較してみましょう。最初のものはsetIntervalを使用しています。
let i = 1;
setInterval(function() {
func(i++);
}, 100);
2番目はネストされたsetTimeoutを使用しています。
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
setIntervalの場合、内部スケジューラは100msごとにfunc(i++)を実行します。
気づきましたか?
setIntervalのfunc呼び出し間の実際の遅延は、コードよりも短くなっています!
これは正常です。なぜなら、funcの実行にかかった時間は、間隔の一部を「消費」しているからです。
funcの実行が予想よりも長く、100ms以上かかる可能性があります。
この場合、エンジンはfuncが完了するのを待ってから、スケジューラをチェックし、時間が来たらすぐに再度実行します。
極端なケースとして、関数が常にdelaymsより長く実行される場合、呼び出しは全く中断せずに発生します。
そして、ネストされたsetTimeoutの図はこちらです。
ネストされたsetTimeoutは、固定遅延(ここでは100ms)を保証します。
これは、新しい呼び出しが前の呼び出しの最後に計画されるためです。
関数がsetInterval/setTimeoutで渡されると、内部参照が作成され、スケジューラに保存されます。これにより、関数への他の参照がなくても、関数はガベージコレクションから保護されます。
// the function stays in memory until the scheduler calls it
setTimeout(function() {...}, 100);
setIntervalの場合、関数はclearIntervalが呼び出されるまでメモリに残ります。
副作用があります。関数は外部レキシカル環境を参照するため、関数が存在する限り、外部変数も存在します。これらは関数自体よりもはるかに多くのメモリを消費する可能性があります。そのため、スケジュールされた関数が不要になったら、それが非常に小さい場合でも、キャンセルする方が良いでしょう。
ゼロ遅延setTimeout
特別なユースケースがあります。setTimeout(func, 0)、または単にsetTimeout(func)です。
これは、できるだけ早くfuncの実行をスケジュールします。しかし、スケジューラは、現在実行中のスクリプトが完了した後にのみ、それを呼び出します。
したがって、関数は現在のスクリプトの「直後」に実行されるようにスケジュールされます。
例えば、これは「Hello」を出力し、すぐに「World」を出力します。
setTimeout(() => alert("World"));
alert("Hello");
最初の行は「0ms後に呼び出しをカレンダーに入れます」。しかし、スケジューラは現在のスクリプトが完了した後にのみ「カレンダーをチェックする」ため、「Hello」が先で、「World」はその後です。
ゼロ遅延タイムアウトには、ブラウザ関連の高度なユースケースもあります。これについては、イベントループ:マイクロタスクとマクロタスクの章で説明します。
ブラウザでは、ネストされたタイマーが実行できる頻度に制限があります。HTML Living Standardには、「5つのネストされたタイマーの後、間隔は少なくとも4ミリ秒に強制されます」と記載されています。
以下の例でそれが何を意味するかを示しましょう。その中のsetTimeout呼び出しは、ゼロ遅延で自身を再スケジュールします。各呼び出しは、times配列に前のものからの実際の時刻を記憶します。実際の遅延はどのようになりますか?見てみましょう。
let start = Date.now();
let times = [];
setTimeout(function run() {
times.push(Date.now() - start); // remember delay from the previous call
if (start + 100 < Date.now()) alert(times); // show the delays after 100ms
else setTimeout(run); // else re-schedule
});
// an example of the output:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
最初のタイマーはすぐに実行され(仕様に記載されているとおり)、その後9, 15, 20, 24...が表示されます。呼び出し間の4ms以上の必須遅延が有効になります。
setTimeoutの代わりにsetIntervalを使用した場合も同様です。setInterval(f)は、ゼロ遅延でfを数回実行し、その後4ms以上の遅延で実行します。
この制限は古くから存在し、多くのスクリプトがそれに依存しているため、歴史的な理由で存在しています。
サーバサイドJavaScriptの場合、この制限は存在せず、Node.jsのsetImmediateなど、即時非同期ジョブをスケジュールする他の方法があります。したがって、このメモはブラウザ固有です。
まとめ
- メソッド
setTimeout(func, delay, ...args)とsetInterval(func, delay, ...args)を使用すると、delayミリ秒後にfuncを1回/定期的に実行できます。 - 実行をキャンセルするには、
setTimeout/setIntervalによって返された値を使用してclearTimeout/clearIntervalを呼び出す必要があります。 - ネストされた
setTimeout呼び出しは、setIntervalよりも柔軟性の高い代替手段であり、実行間の時間をより正確に設定できます。 setTimeout(func, 0)(setTimeout(func)と同じ)によるゼロ遅延スケジューリングは、「できるだけ早く、ただし現在のスクリプトが完了した後に」呼び出しをスケジュールするために使用されます。- ブラウザは、5回以上のネストされた
setTimeout呼び出し、またはsetInterval(5回目の呼び出し後)の最小遅延を4msに制限しています。これは歴史的な理由によるものです。
すべてのスケジューリングメソッドが正確な遅延を保証するわけではないことに注意してください。
例えば、ブラウザ内のタイマーは多くの理由で遅くなる可能性があります。
- CPUが過負荷になっている。
- ブラウザのタブがバックグラウンドモードになっている。
- ラップトップがバッテリー節約モードになっている。
これらすべてにより、ブラウザとOSレベルのパフォーマンス設定に応じて、最小タイマー解像度(最小遅延)が300msまたは1000msに増加する可能性があります。
コメント
<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。