0ミリ秒、お待ちください〜
クライアントサイドのJavaScriptを使っていると、setTimeout()
はそこそこ使う機会が来る関数かと思います。これは、
- 実行する関数
- 待機時間(ミリ秒単位)
という2つの引数を取って、指定時間後に関数を実行する機能です。…そこまではいいのですが、ときどき第2引数に0を指定するような使い方があります。「0ミリ秒に実行」だと、そのまま実行しても何も変わらないような気がするのですが、いったいどういう意味があるのでしょうか。
JavaScriptはシングルスレッド
postMessage
やWeb Workerといった一部の機能を除いて、ブラウザ上のJavaScriptはシングルスレッドで動いています。つまり、関数に入っていないトップレベルコードなら読み込まれた時点から実行完了するまで、コールバック関数であれば呼び出されてからその関数を抜けるまで、そこにあるコードしか実行できません。
そういうわけで、その分割できない1ブロックで大量の処理を行おうとしてしまうと、ブラウザは入力すら受け付けない状態になり、さらに続けば「処理を中止しますか?」ダイアログを召喚することになってしまいます。
処理分割のためのsetTimeout
純粋なデータ処理であればWeb Workerに回す方法もありますが、DOM操作が多い場合にはそうも行きません。ただ、もちろんブラウザを固めたりしてはクライアントコードとして失格なので、setTimeout
を使うことで処理を分割する手法が存在します。
内部動作
じつは、setTimeout
はJavaScriptの言語機能ではなくてブラウザ側の機能です。これも一種のイベントコールバックだと考えるとスッキリします。
- setTimeoutで関数を登録する
- 登録した関数から抜けてブラウザに処理が戻る
- ブラウザが(他の処理をしつつ)タイミングが来れば関数を呼び出す
0ミリ秒で登録した場合にも、いったんブラウザに戻ってからになるので、JavaScriptの実行中に溜まったイベントの処理なども行われます。
大量のDOM操作に使う
例えば、テーブルのセル1000個が入ったDOM配列があって、1セルごとに違った処理をしないといけないけど、1つに付き5ミリ秒かかってしまうとします。そのまま真っ正直に実行すれば5秒かかってしまい、操作性を落としてしまいます。このようなときに、setTimeout
を使うことで、連続した実行時間を分割できます。
//tdsが、大量のテーブルセルが入ったDOM配列
var length=tds.length,start=0;
var MAX_EXECUTION_COUNT=100;
setTimeout(function loop(){
for(var i=start;(i<start+MAX_EXECUTION_COUNT) && i<length ;++i){
//tds[i]を使って作業をする
}
if(i<length){
start += MAX_EXECUTION_COUNT;
setTimeout(loop,0);
}
},0);
よく、関数の引数として渡す関数リテラルが「無名関数」と呼ばれますが、上の例のように名前を付けることもできます(この名前は関数の中でのみ有効で、外のスコープを汚すことはありません)。
何かを待機する
もちろんコールバックがあるようなものであればそれで受け止めればいいのですが、そうでないものを待つ場合はsetTimeout
でブラウザに戻しつつ待たないとJavaScriptのループしか実行しなくなってしまって、待つべきイベントが実行されないことになってしまいます。
ここでは、とあるライブラリの動的ロードを待ってみます。
//正しく書くのが面倒なので、jQueryで書く
$('<script></script>').attr('src','https://some-cdn.com/some_library.js')
.appendTo('head');
//上の読み込みが済んでから実行するコード
(function onLoad(){
if(!window.SomeLibrary){
setTimeout(onLoad,0);
return;
}
//以下、SomeLibraryを使ったコード
SomeLibrary.useFeature();
})(); //その場で実行
もちろん、1ミリ秒でも早く実行したいのでなければ、0でなくてタイムラグを付けることも可能です。
余談
setTimeout(0)
と書いても、実際には最低4ミリ秒の遅延が起きるとのことです。あと、setTimeout
は数値を返して、その返り値をclearTimeout
に渡せば実行を阻止できますが、あまり使う機会は来ない気もします(実行が終わった後に渡してもエラーは起きませんが、もちろん後の祭りです)。