JavaScriptは奥が深い。
約1年くらい使ってきたが、ワテの知らない機能がまだまだ沢山あるようだ。
今回のテーマは、
「JavaScriptでSetTimeoutを使って他の処理が終わったかどうか調べる」
だ。
例えば、JavaScriptでプログラムを書いていると、何かの変数を使おうとしたらまだ中身がセットされていなくてundefinedでエラーが出るなどの状況は良くある。
そんな問題を回避する方法をこの記事では紹介してみたい。
では本題に入ろう。
なぜ他の処理が終わったかどうか調べる必要があるのか?
例えば、なんらかの非同期処理を実行して、それが完了した時点でその変数に値が入る場合では、その処理が終わる前にその変数を使おうとすると中身はundefinedだ。
又あるいは、postMessageでメッセージが送信されて来るので、それを受信してメッセージで送られてきたデータから値を取り出して変数にセットするなどの場合もメッセージ受信前は当然undefinedとなる。
その他にもいろいろある。
Promiseを使うと非同期処理の完了を待ってから同期処理を再開できる
前者の非同期処理実行の場合にはPromiseと言う機能などを使えば、非同期処理が終わった時点で確実に次の処理が行えるので便利だ。
JavaScriptのPromiseで同期処理と非同期処理のタイミングを制御する(便利)
後者のpostMessage受信の場合には、メッセージを受信するイベントハンドラ関数
addEventListener()
を使えば、メッセージを受信した時点で自分で変数の値をセットすれば良い。
その他の方法としては、ワテが知っているのは、
JavaScriptで変数が変更されたタイミングで何か処理をしたい(解決)
などの方法もある。
これらの手法を使うと、今から自分が使おうとしている変数には確実に目的の値が入っているように出来るので、undefinedというエラーが出る事は無いと思う。
しかしながら、上記の例はどれもそのイベントが起こると確実に検出する方法が用意されているので、それらの手法を使えばイベント完了のタイミングで必要な作業をすれば良いのだが、JavaScriptで色んなプログラムを書いていると中にはそういう状況に当てはまらない場合もある。
いつ起こるか分からないイベントをタイマーでループしながら検出する
ある変数の中身を誰かがセットするのだが、いつ行われるのか分からない。
どんな状況でそういうケースがあるのかは例を挙げるときりがないが、いざ、ここで具体例を挙げようとすると良い例が思いつかない。
なので、例えば入力欄のテキストが変更されるタインミングを待つことにしてみた。
なお、そういう場合には
onchange onkeydown
などのイベントハンドラを使えばそれで簡単に行けるので、ここでやっているようなヘンテコな事はしなくても良いが、あくまで、いつ起こるか分からないイベントをシミュレーションするという意味で、この例を示しています。
上のボタンをクリック後に↓の入力欄の文字を適当に変更して下さい。
入力欄が変更されたかどうかsetTimeout()関数を使って1秒ごとに確認して、変更されたら処理を終わる。
ただし、上限を10回に設定しているので、
最大 = 1秒 x 10回 = 10秒
経過すると処理を中断する。
そのプログラムはこんな感じ
即席で作ったので改善の余地も多いが。
 function repeat_n(myfunc, dataObj, millisec, n_max, n_now, is_end_cond_func) {
 
            // myfunc,           毎回実行する関数。無ければ function(){}
            // dataObj,          ↑の関数に渡すデータ。無ければnull
            // millisec,         繰り返しの間隔。
            // n_max,            繰り返し回数マックス
            // n_now,            繰り返し初期値0。引数にする必要なかったかも。
            // is_end_cond_func  終了判定関数。is_end_cond_func(n_now)のように繰り返し
            //                   カウンタ値を一個だけ引数を取るようにしている。なので、それ
            //                   以外のデータを渡したい場合は、引数をobject型にするなど。
 
            setTimeout(function (n_now) {
                if (n_now === n_max)
                    return;
 
                if (is_end_cond_func(n_now))
                    return;
 
                n_now++;
                myfunc(n_now, dataObj);
                repeat_n(myfunc, dataObj, millisec, n_max, n_now, is_end_cond_func);
 
            }, millisec, n_now);
        };
 var myfunc = function (n_now, dataObj) {
            var n_max = dataObj.n_max;
            document.getElementById('n_now_ID').innerText
                = n_now + '/' + n_max + '回目';
        }
 function clearTimerAll() {
     var id = window.setTimeout(function () { }, 0);
     while (id--) {
         window.clearTimeout(id);
     }
 }
 var is_end_cond = function () {
     var val_now = document.getElementById('inputVal_ID').value;
     if (val_now === '初期値') {
         return false;
     } else {
         document.getElementById('n_now_ID').innerText
             = '終了条件が達成されたので終わる。';
         return true;
     }
 };
 function button_click_CB() {
     clearTimerAll();
     document.getElementById('inputVal_ID').value = '初期値';
     var n_max = 10;
     repeat_n(
         myfunc,
         { n_max: n_max },   // dataObj
         1000,               // milli sec
         n_max,              // max
         0,                  // n_now
         is_end_cond         // is_end_condition
     );
 }
複雑そうに見えるが、処理は簡単だ。
ボタンがクリックされるとbutton_click_CB()がコールされ、その中でrepeat_n()と言う関数を実行している。
引数が多くてややこしそうに見えるが、引数の意味は以下の通り。
repeat_n()関数
function repeat_n(myfunc, dataObj, millisec, n_max, n_now, is_end_cond_func)
| 引数 | 意味 | 
|---|---|
| myfunc | 毎回実行する関数。無ければ function(){} | 
| dataObj | myfunc関数に渡すデータ。データ型は任意なのでstringでも良いが、沢山のデータを渡したい場合にはobject型にして好きなデータを入れればよい。渡すデータが無ければnull。 | 
| millisec | 繰り返しの間隔、ミリ秒 | 
| n_max | 繰り返し回数マックス | 
| n_now | 繰り返し初期値0。引数にする必要なかったかも。 | 
| is_end_cond_func | 終了判定関数。is_end_cond_func(n_now)のように繰り返しカウンタ値を一個だけ引数を取るようにしている。なので、それ以外のデータを渡したい場合は、引数をobject型にするなどが良いかも。要検討。 | 
myfunc, is_end_cond_funcという二つの関数があるが、必要なのは後者の終了判定関数のみだ。そのis_end_cond_funcの中で、終了条件に達した場合にtrueをリターン、それ以外ならfalseをリターンするようにする。
このis_end_cond_func関数は、必要に応じて自作する必要がある。
一方、myfuncは、必須ではないが、毎回の終了判定の確認処理の中で何かついでに実行したい場合に使うと良い。
ここの例では、myfunc()の中で毎回表示を更新するようにしているが、そういう処理が必要なくて、単に終了判定のみで良ければ、こんな感じ。
var n_max = 10;
repeat_n(
    function(){},       // myfunc
    null,               // dataObj
    1000,               // milli sec
    n_max,              // max
    0,                  // n_now
    is_end_cond         // is_end_condition
);
上記の例では、is_end_cond関数は別に定義しているが、もし関数が短い場合には、その関数自体をrepeat_nの引数内に埋め込んでも良い。
SetTimeoutの第三引数
普通は、以下のように二つの引数を与えて使う場合が多い。
setTimeout(code, delay);
一方、下のように
setTimeout(func, delay[, param1, param2, ...]);
第三番目以降にパラメータを指定して渡す事が出来るらしいので、その方法をワテのプログラムでも利用している。
あとはsetTimeoutの中から再帰的にsetTimeoutを実行し、その実行の度に終了条件が達成されたのかどうかを確認している。C,C++などの言語でこんなふうに関数内から自分自身を再帰的に呼び出すとstack overflowエラーが出るが、JavaScriptのSetTimeout関数の場合にはそうはならない。詳しい事は良く知らないが、setTimeoutが実行される度に、過去を引き継がずに毎回新規に実行されている感じらしい。
定期的に処理を行いたい場合には、似たような関数にsetInterval()と言うのがあるが、ワテは何となくsetTimeout()しか使わない。理由は、setInterval()は自分で止めない限り動き続ける点が何となく嫌いなので。まあ、そんな事は読者の皆さんには関係ないが。
と言う事で、まとめに入る。
まとめ
この記事では、JavaScriptのsetTimeout()関数を使って一定時間毎に特定の変数の値を調べて、その変数が目的の値に変更されたのかどうかを検出する機能を紹介した。
ワテの場合、JavaScriptを始めてまだ一年なので、まだまだ勉強中であるので、ここで紹介したようなヘンテコな事をやらなくても、もっとスマートな手法があるかもしれません。
何かそういう情報が有りましたらお教え頂けると有り難いです。
ほんまに三日でマスター出来るのか⁉
ワテの場合、一年掛かってもまだまだ序の口だ。





コメント