LoginSignup
11
4

More than 1 year has passed since last update.

JavaScript: setTimeoutは初心者泣かせ?よくある失敗例をまとめてみた

Last updated at Posted at 2020-12-20

はじめに

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でも即時に実施されない

B->Aの順で出力される。
setTimeout(function () {
  console.log('A');
}, 0);
console.log('B');

0msec(待機しない)を指定しても、必ずABの後に実行される。Aを表示する処理はタスクキューにいったん入れられて、全ての処理が終わった後に初めてタスクキューの処理が実施されるからである。

例2: setTimeoutの実行順ではなく、タスクキューへの登録順

B->C->Aの順で出力される。
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: タスクキューの処理がなかなか開始されないケース

待機時間0秒だと思ったが、実際はBが表示された後にAが表示されるまで5秒間かかる(実際はタスクキューに入るまでが0秒だというだけ)
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の問題と言ったほうがいいかもしれないが、いちおう補足として書いておく。

3,3,3と出力される。
for (i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i)
  }, 1000);
}

これは、変数iがグローバルスコープのため、console.log(i)の処理が3つキューに入れられて実際に表示しようとした段階で、ループが完了してi=3になってしまっているため、全部3と表示されてしまうという問題である。

0,1,2と出力される。
for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i)
  }, 1000);
}

このようにletで書いておけば、変数iはループ間で共有されないため、メインスレッドが終わっても他の値に変わっていることはない。そのため、期待通りの動きになる(この記事も参照。)

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4