1001

私は最近、コードが<select>JavaScript を介して動的にロードされていた、かなり厄介なバグに遭遇しました。この動的にロードされ<select>たものには、事前に選択された値がありました。IE6 では、次のように、の値が selectedの属性と同期しない場合が<option>あるため、 selected を修正するコードが既にありました。<select>selectedIndex<option>index

field.selectedIndex = element.index;

ただし、このコードは機能しませんでした。フィールドselectedIndexが正しく設定されていたとしても、間違ったインデックスが選択されてしまうことがありました。ただし、alert()適切なタイミングでステートメントを挿入すると、正しいオプションが選択されます。これはある種のタイミングの問題かもしれないと考えて、以前にコードで見たことのあるランダムなことを試しました。

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

そして、これはうまくいきました!

問題の解決策はありますが、これで問題が解決する理由が正確にわからないので不安です。誰も公式の説明を持っていますか?を使用して「後で」関数を呼び出すことで回避しているブラウザの問題は何setTimeout()ですか?

4

19 に答える 19

914

質問では、次の間に競合状態が存在しました。

  1. ブラウザーがドロップダウン リストを初期化し、選択したインデックスを更新する準備ができていること、および
  2. 選択したインデックスを設定するコード

あなたのコードは一貫してこの競争に勝ち、ブラウザの準備が整う前にドロップダウン選択を設定しようとしました。つまり、バグが発生しました。

この競合が存在するのは、JavaScript がページ レンダリングと共有される単一の実行スレッドを持っているためです。実際には、JavaScript を実行すると DOM の更新がブロックされます。

あなたの回避策は次のとおりです。

setTimeout(callback, 0)

コールバックをsetTimeout使用して呼び出し、2 番目の引数として 0 を指定すると、可能な限り最短の遅延 (タブにフォーカスがあり、実行の JavaScript スレッドがビジーでない場合は約 10 ミリ秒) の後に、コールバックが非同期で実行されるようにスケジュールされます。

したがって、OPの解決策は、選択したインデックスの設定である約10ミリ秒遅らせることでした。これにより、ブラウザーが DOM を初期化する機会が与えられ、バグが修正されました。

Internet Explorer のすべてのバージョンは奇妙な動作を示し、この種の回避策が必要な場合がありました。あるいは、OP のコードベースの本物のバグだった可能性もあります。


Philip Robertsの講演「イベント ループとは何ですか?」を参照してください。より完全な説明のために。

于 2009-04-23T00:14:17.927 に答える
703

序文:

他のいくつかの答えは正しいですが、解決されている問題が実際に何であるかを説明していないので、私はその詳細な図を提示するためにこの答えを作成しました。

そのため、ブラウザの機能と使用方法の詳細なウォークスルーをsetTimeout()投稿しています。長く見えますが、実際には非常に単純でわかりやすいものです。非常に詳細に作成しました。

更新:私はJSFiddleをライブで作成しました-以下の説明を示します:http://jsfiddle.net/C2YBE/31/。キックスタートを手伝ってくれた@ThangChungに感謝します

UPDATE2: JSFiddle Webサイトが停止したり、コードが削除されたりした場合に備えて、最後にこの回答にコードを追加しました。


詳細

「何かをする」ボタンと結果divを備えたWebアプリを想像してみてください。

onClick「何かをする」ボタンのハンドラーは、2つのことを行う関数「LongCalc()」を呼び出します。

  1. 非常に長い計算を行います(たとえば3分かかります)

  2. 計算結果を結果divに出力します。

これで、ユーザーはこれをテストし始め、[何かをする]ボタンをクリックすると、ページは3分間何もしないように見えます。落ち着きがなくなり、ボタンをもう一度クリックし、1分待って、何も起こらず、ボタンをもう一度クリックします...

問題は明らかです。何が起こっているかを示す「ステータス」DIVが必要です。それがどのように機能するか見てみましょう。


したがって、「ステータス」DIV(最初は空)を追加し、onclickハンドラー(関数LongCalc())を変更して次の4つのことを行います。

  1. ステータス「計算中...3分ほどかかる場合があります」をステータスDIVに入力します

  2. 非常に長い計算を行います(たとえば3分かかります)

  3. 計算結果を結果divに出力します。

  4. ステータス「計算完了」をステータスDIVに入力します

そして、ユーザーにアプリを提供して再テストしてもらいます。

彼らは非常に怒っているようにあなたに戻ってきます。そして、ボタンをクリックしたときに、ステータスDIVが「計算中...」ステータスで更新されなかったことを説明します!!!


あなたは頭をかいて、StackOverflowで質問し(またはドキュメントやグーグルを読んで)、問題に気づきます:

ブラウザは、イベントから生じるすべての「TODO」タスク(UIタスクとJavaScriptコマンドの両方)を単一のキューに配置します。残念ながら、新しい「Calculating ...」値を使用して「Status」DIVを再描画することは、キューの最後に行く別のTODOです。

ユーザーのテスト中のイベントの内訳、各イベント後のキューの内容は次のとおりです。

  • 列:[Empty]
  • イベント:ボタンをクリックします。イベント後のキュー:[Execute OnClick handler(lines 1-4)]
  • イベント:OnClickハンドラーの最初の行を実行します(たとえば、ステータスDIV値を変更します)。イベント後のキュー:[Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]DOMの変更は瞬時に行われますが、対応するDOM要素を再描画するには、キューの最後で発生したDOMの変更によってトリガーされる新しいイベントが必要であることに注意してください
  • 問題!!! 問題!!!詳細は以下に説明します。
  • イベント:ハンドラーの2行目を実行します(計算)。後のキュー:[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
  • イベント:ハンドラーで3行目を実行します(結果DIVを入力します)。後のキュー:[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
  • イベント:ハンドラーの4行目を実行します(ステータスDIVに「DONE」を入力します)。キュー:[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
  • イベント:ハンドラーサブreturnから暗黙的に実行します。onclick「ExecuteOnClickハンドラー」をキューから外し、キューの次のアイテムの実行を開始します。
  • 注:計算はすでに終了しているため、ユーザーはすでに3分経過しています。再描画イベントはまだ発生していません!!!
  • イベント:「計算中」の値でステータスDIVを再描画します。再描画を行い、それをキューから外します。
  • イベント:結果値を使用して結果DIVを再描画します。再描画を行い、それをキューから外します。
  • イベント:「完了」値でステータスDIVを再描画します。再描画を行い、それをキューから外します。鋭い目の視聴者は、「計算中」の値がマイクロ秒の何分の1かで点滅している「ステータスDIV」に気付くかもしれません-計算が終了した後

したがって、根本的な問題は、「Status」DIVの再描画イベントが、3分かかる「executeline 2」イベントの後、最後にキューに配置されるため、実際の再描画が行われるまでは発生しないことです。計算が行われた後。


救助に来るsetTimeout()。それはどのように役立ちますか?を介して長時間実行コードを呼び出すことによりsetTimeout、実際には2つのイベントが作成されます。setTimeout実行自体と(タイムアウトが0であるため)、実行中のコードのキューエントリを分離します。

したがって、問題を修正するには、onClickハンドラーを2つのステートメント(新しい関数または内のブロック内onClick)になるように変更します。

  1. ステータス「計算中...3分ほどかかる場合があります」をステータスDIVに入力します

  2. setTimeout()0タイムアウトとLongCalc()関数の呼び出しで実行します。

    LongCalc()機能は前回とほぼ同じですが、最初のステップとして「計算中...」ステータスDIVの更新がないことは明らかです。代わりに、すぐに計算を開始します。

では、イベントシーケンスとキューは現在どのようになっていますか?

  • 列:[Empty]
  • イベント:ボタンをクリックします。イベント後のキュー:[Execute OnClick handler(status update, setTimeout() call)]
  • イベント:OnClickハンドラーの最初の行を実行します(たとえば、ステータスDIV値を変更します)。イベント後のキュー:[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
  • イベント:ハンドラーで2行目を実行します(setTimeout呼び出し)。後のキュー:[re-draw Status DIV with "Calculating" value]。キューには、あと0秒間、新しいものは何もありません。
  • イベント:0秒後にタイムアウトからのアラームが鳴ります。後のキュー:[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
  • イベント:「計算中」の値でステータスDIVを再描画します。後のキュー:[execute LongCalc (lines 1-3)]。この再描画イベントは、アラームが鳴る前に実際に発生する可能性があることに注意してください。これも同様に機能します。
  • ..。

やったー!計算が開始される前に、ステータスDIVが「計算中...」に更新されました!!!



以下は、これらの例を示すJSFiddleのサンプルコードです。http://jsfiddle.net/C2YBE/31/

HTMLコード:

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td>
    </tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td>
    </tr>
</table>

JavaScriptコード:(実行されonDomReady、jQuery 1.9が必要になる場合があります)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
于 2011-01-01T17:53:17.850 に答える
93

JavaScriptタイマーのしくみに関するJohnResigの記事をご覧ください。タイムアウトを設定すると、エンジンが現在の呼び出しスタックを実行するまで、非同期コードが実際にキューに入れられます。

于 2010-12-06T21:38:30.173 に答える
26

setTimeout()が 0 に設定されている場合でも、DOM 要素が読み込まれるまでの時間を稼ぎます。

これをチェックしてください:setTimeout

于 2009-04-22T21:52:10.047 に答える
25

ブラウザーには「メイン スレッド」と呼ばれるプロセスがあり、いくつかの JavaScript タスク、UI の更新 (ペイント、再描画、リフローなど) の実行を担当します。JavaScript タスクはメッセージ キューに入れられ、ブラウザーのメイン スレッドにディスパッチされます。実行されました。メイン スレッドがビジー状態のときに UI の更新が生成されると、タスクがメッセージ キューに追加されます。

于 2013-02-25T21:27:03.443 に答える
23

ここには相反する賛成の答えがあり、証拠がなければ誰を信じるべきかを知る方法はありません. これは、@DVK が正しく、@SalvadorDali が正しくないという証拠です。後者は次のように主張しています。

「そして、ここに理由があります: 0 ミリ秒の時間遅延で setTimeout を設定することはできません。最小値はブラウザーによって決定され、0 ミリ秒ではありません。歴史的に、ブラウザーはこの最小値を 10 ミリ秒に設定しますが、HTML5 の仕様と最近のブラウザでは 4 ミリ秒に設定されています。」

4 ミリ秒の最小タイムアウトは、何が起こっているのかには関係ありません。実際に起こることは、setTimeout がコールバック関数を実行キューの最後にプッシュすることです。setTimeout(callback, 0) の後に実行に数秒かかるブロッキング コードがある場合、コールバックはブロッキング コードが終了するまで数秒間実行されません。このコードを試してください:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

出力は次のとおりです。

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033
于 2014-07-26T00:30:21.370 に答える
17

これを行う理由の 1 つは、コードの実行を別の後続のイベント ループに延期することです。ある種のブラウザー イベント (マウス クリックなど) に応答する場合、現在のイベントが処理された後にのみ操作を実行する必要がある場合があります。施設はそれsetTimeout()を行う最も簡単な方法です。

2015年になったので編集します。まったく同じではありませんが、言及する価値があるほどrequestAnimationFrame()十分に近いです。setTimeout(fn, 0)

于 2011-01-01T17:38:04.523 に答える
13

これは、古い回答を持つ古い質問です。この問題に新たな見方を加えて、なぜこれが役立つのかではなく、なぜこれが起こるのかを答えたいと思いました。

したがって、次の 2 つの機能があります。

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

f1(); f2();次に、2番目のものが最初に実行されたことを確認するために、次の順序でそれらを呼び出します。

setTimeoutその理由は次のとおりです。時間遅延を 0 ミリ秒にすることはできません。最小値はブラウザによって決定され、 0ミリ秒ではありません。歴史的に、ブラウザーはこの最小値を 10 ミリ秒に設定していますが、HTML5 仕様と最新のブラウザーでは 4 ミリ秒に設定されています。

ネスト レベルが 5 より大きく、タイムアウトが 4 より小さい場合は、タイムアウトを 4 に増やします。

また、モジラから:

最新のブラウザーで 0 ミリ秒のタイムアウトを実装するには、ここで説明されているように window.postMessage() を使用できます。

PS情報は以下の記事を読んでからとります。

于 2014-05-19T21:34:47.610 に答える
8

の期間が渡されているので、に渡されたコードを実行フローから0削除するためだと思います。setTimeoutしたがって、時間がかかる可能性のある関数であれば、後続のコードの実行を妨げることはありません。

于 2011-01-01T17:39:29.277 に答える
3

これが行うもう1つのことは、関数呼び出しをスタックの一番下にプッシュし、関数を再帰的に呼び出している場合にスタックオーバーフローを防ぐことです。これにはwhileループの効果がありますが、JavaScriptエンジンが他の非同期タイマーを起動できるようにします。

于 2012-06-21T01:14:43.583 に答える
2

setTimeout を呼び出すことで、ユーザーが行っていることにページが反応する時間を与えます。これは、ページの読み込み中に実行される関数に特に役立ちます。

于 2009-04-22T21:53:43.553 に答える
2

setTimeout が役立つその他のケース:

ブラウザが「フリーズ」したように見えたり、「ページ上のスクリプトがビジーです」と表示されたりしないように、実行時間の長いループまたは計算を小さなコンポーネントに分割する必要があります。

クリックされたときにフォーム送信ボタンを無効にしたいのですが、onClick ハンドラーでボタンを無効にすると、フォームは送信されません。時間がゼロの setTimeout はトリックを実行し、イベントを終了させ、フォームの送信を開始し、ボタンを無効にすることができます。

于 2012-12-14T13:09:00.043 に答える
1

実行ループと、他のコードが完了する前の DOM のレンダリングに関する回答は正しいです。JavaScript の 0 秒のタイムアウトは、コードを疑似マルチスレッド化するのに役立ちますが、そうではありません。

JavaScript でのクロス ブラウザー/クロス プラットフォームのゼロ秒タイムアウトのベスト値は、実際には 0 (ゼロ) ではなく約 20 ミリ秒です。これは、多くのモバイル ブラウザーがクロックの制限により 20 ミリ秒未満のタイムアウトを登録できないためです。 AMDチップ上。

また、DOM 操作を伴わない実行時間の長いプロセスは、JavaScript の真のマルチスレッド実行を提供するため、Web ワーカーに送信する必要があります。

于 2013-03-13T23:29:55.870 に答える