#はじめに
JavaScriptのsetTimeout
関数は、記事によっては「タイマー処理」だとか「何秒か遅らせて実行したい時に利用する」と紹介されていることがあるが、これは正確な説明ではないように思える。
(私のような)初心者は、そもそもJavaScriptの非同期処理というものをよくわかっておらず、こういう説明をそのまま鵜呑みにして返って痛い目に遭って他の記事を探すということを繰り返してきた。結局JavaScriptの非同期処理についての正確な理解が最短の道なのだが・・・そのためには、JavaScriptの非同期処理をできる限り正確に理解するがとてもわかりやすいので一読を勧める。
結論からいうと、
-
setTimeout
の第1引数はタスクキューに入れて後から実施する関数を記載する。 -
setTimeout
は第2引数に**タスクキューに入れられるまでの待機時間(単位: msec)**を指定する。- この時間を待機した後は、いったんタスクキューに入れられて実行待ち状態になる。
- 実行待ち状態だからといって、すぐに実行されるとは限らない。もしメインスレッドで他に実施する処理がなくなった段階で、ようやくタスクキューにたまっている処理がFirst-In First-Outで処理されていく。逆に言うと、もしメインスレッドでとても重い処理があれば、このタスクキューに格納された処理はその分待たされる。つまり、実際に実行されるまでの時間は、第2引数で指定した時間よりずっと長くなる可能性もある。
#第1引数に対するよくやる失敗。
- 関数を指定してしないといけないのに、関数を実行してしまっている。
function fn1(value) {
console.log(value);
}
setTimeout(fn1(5), 5000); //5秒待たずに即時に実行されてしまう。
この場合、fn1(5)を実行することによって、(return文が書かれていないため)fn1(5)はundefinedを返す。つまり、setTimeout(undefined, 5000)
が実行されたのと同じであり、実質機能していない。
第1引数に関数を指定してあげるために、
function fn1(value) {
console.log(value);
}
setTimeout(function(){fn1(5)}, 5000);
のように関数定義をしてfn1(5)をラップしてもいいのだが、そもそもsetTimeoutに引数を渡すために第3引数が用意されているため、以下のように記述するのが普通である。
function fn1(value) {
console.log(value);
}
setTimeout(fn1, 5000, 5);
#第2引数に対する誤解。
##例1: 待機時間0でも即時に実施されない
setTimeout(function () {
console.log('A');
}, 0);
console.log('B');
0msec(待機しない)
を指定しても、必ずA
はB
の後に実行される。A
を表示する処理はタスクキューにいったん入れられて、全ての処理が終わった後に初めてタスクキューの処理が実施されるからである。
##例2: setTimeoutの実行順ではなく、タスクキューへの登録順
setTimeout(function () {
console.log('A');
}, 2000);
setTimeout(function () {
console.log('C');
}, 1000);
console.log('B');
A
を出力するsetTimeoutが先に実行されたからといって、A
が先に出力されるわけではない。A
は2秒後にタスクキューに登録され、C
は1秒後にタスクキューに登録されるため、タスクキューに登録されたのはA
よりC
の方が先である。そのため、A
よりC
が先に出力される。
##例3: タスクキューの処理がなかなか開始されないケース
setTimeout(function () {
console.log('A');
}, 0)
console.log('B');
const startTime = new Date();
while (new Date() - startTime < 5000); //5秒間ループを続ける
A
は即時にタスクキューに登録され、次にB
が出力される。
しかし、その後にずっとメインスレッドでの処理が行われている(5秒間のループ)ため、その間はタスクキューの処理は実施されない。メインスレッドでの処理が終わって初めてA
が表示される。setTimeoutの待機時間が0msecであったとしても、B
の前にA
が表示されるわけでもなく、B
が表示された直後にA
がすぐ表示されるわけでもない。
##例4: varを変数に利用したケース
これは、setTimeoutの問題というよりvar
/let
の問題と言ったほうがいいかもしれないが、いちおう補足として書いておく。
for (i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000);
}
これは、変数iがグローバルスコープのため、console.log(i)
の処理が3つキューに入れられて実際に表示しようとした段階で、ループが完了してi=3になってしまっているため、全部3と表示されてしまうという問題である。
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 1000);
}
このようにletで書いておけば、変数iはループ間で共有されないため、メインスレッドが終わっても他の値に変わっていることはない。そのため、期待通りの動きになる(この記事も参照。)