web-dev-qa-db-ja.com

実行時間の長い関数でのDOMの更新

クリックすると長時間実行される機能を実行するボタンがあります。今、関数の実行中にボタンのテキストを変更したいのですが、FirefoxやIEなどの一部のブラウザーで問題が発生します。

html:

<button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">do some work</span></button>

javascript:

function longrunningfunction() {
    document.getElementById("myspan").innerHTML = "doing some work";
    document.getElementById("mybutt").disabled = true;
    document.getElementById("mybutt").className = "buttonDisabled";

    //long running task here

    document.getElementById("myspan").innerHTML = "done";
}

今、これはFirefoxとIEで問題があります(chromeそれは問題なく動作します)

だから私はそれをsettimeoutに入れることを考えました:

function longrunningfunction() {
    document.getElementById("myspan").innerHTML = "doing some work";
    document.getElementById("mybutt").disabled = true;
    document.getElementById("mybutt").className = "buttonDisabled";

    setTimeout(function() {
        //long running task here
        document.getElementById("myspan").innerHTML = "done";
    }, 0);
}

しかし、これはFirefoxでも機能しません!ボタンは無効になり、色が変わります(新しいcssの適用により)が、テキストは変わりません。

時間を設定するには、時間を0ミリ秒ではなく50ミリ秒に設定する必要があります(ボタンのテキストを変更します)。今、私はこの愚かさを少なくとも見つけます。遅延が0ミリ秒でも機能するかどうかはわかりますが、遅いコンピューターではどうなりますか?多分Firefoxはsettimeoutで100msが必要でしょうか?それはかなり愚かに聞こえます。 1ミリ秒、10ミリ秒、20ミリ秒、何度も試しましたが、更新されません。 50msだけで。

だから私はこのトピックのアドバイスに従いました:

JavaScript DOM操作後のInternet ExplorerでのDOM更新の強制

だから私は試した:

function longrunningfunction() {
    document.getElementById("myspan").innerHTML = "doing some work";
    var a = document.getElementById("mybutt").offsetTop; //force refresh

    //long running task here

    document.getElementById("myspan").innerHTML = "done";
}

ただし、機能しません(FIREFOX 21)。それから私は試しました:

function longrunningfunction() {
    document.getElementById("myspan").innerHTML = "doing some work";
    document.getElementById("mybutt").disabled = true;
    document.getElementById("mybutt").className = "buttonDisabled";
    var a = document.getElementById("mybutt").offsetTop; //force refresh
    var b = document.getElementById("myspan").offsetTop; //force refresh
    var c = document.getElementById("mybutt").clientHeight; //force refresh
    var d = document.getElementById("myspan").clientHeight; //force refresh

    setTimeout(function() {
        //long running task here
        document.getElementById("myspan").innerHTML = "done";
    }, 0);
}

OffsetTopの代わりにclientHeightも試しましたが、何もしませんでした。 DOMは更新されません。

誰かができればハックしない信頼できるソリューションを提供できますか?

前もって感謝します!

ここで提案されているように、私も試しました

$('#parentOfElementToBeRedrawn').hide().show();

無駄に

Chrome/MacでDOMの再描画/更新を強制する

TL; DR:

setTimeoutを使用せずにDOMを強制的に更新するための信頼できるクロスブラウザーメソッドを探します(実行時間の長いコードの種類、ブラウザー、コンピューターの速度に応じて必要な時間間隔が異なるため、推奨される解決策は50〜100ミリ秒の範囲で必要です)状況に応じて)

jsfiddle: http://jsfiddle.net/WsmUh/5/

24
MirrorMirror

解決しました!!setTimeout()!!!

Chrome 27.0.1453、Firefox 21.0、Internet 9.0.8112でテスト済み

$("#btn").on("mousedown",function(){
$('#btn').html('working');}).on('mouseup', longFunc);

function longFunc(){
  //Do your long running work here...
   for (i = 1; i<1003332300; i++) {}
  //And on finish....  
   $('#btn').html('done');
}

デモはこちら!

7
Kylie

Webページは単一のスレッドコントローラーに基づいて更新され、ブラウザーの半分は、JSの実行が停止してブラウザーに計算制御を戻すまで、DOMまたはスタイル設定を更新しません。つまり、いくつかのelement.style.[...] = ...を設定した場合、コードの実行が完了するまで(完全に、またはブラウザが数ミリ秒の間処理をインターセプトできるようにしていることがブラウザに認識されるため)、起動しません。

2つの問題があります。1)ボタンに<span>が含まれています。それを削除し、ボタン自体に.innerHTMLを設定するだけです。しかし、これはもちろん本当の問題ではありません。 2)非常に長い操作を実行しているので、その理由について十分に考え、理由に答えた後、その方法を以下に示します。

長い計算ジョブを実行している場合は、タイムアウトコールバックに切り分けます(または、2019年にはawait/async-このanserの最後にある注を参照してください)。あなたの例はあなたの「長い仕事」が実際に何であるかを示していません(スピンループはカウントされません)が、あなたが取るブラウザに応じていくつかのオプションがあり、1つ[〜#〜]巨大です[〜#〜]ブックノート:JavaScriptで長いジョブを実行しないでください。 JavaScriptは仕様によりシングルスレッド環境であるため、実行する操作はすべてミリ秒単位で完了する必要があります。それができない場合、あなたは文字通り何か間違ったことをしています。

難しいことを計算する必要がある場合は、AJAX操作(ブラウザー間で共通、多くの場合、a)その操作の処理が高速で、b)非同期では実行できない30秒の適切な時間でサーバーにオフロードします。 -結果が返されるのを待つ)、またはWebワーカーのバックグラウンドスレッドを使用する(普遍的ではない)。

計算に時間がかかるが不合理ではない場合は、コードをリファクタリングして、タイムアウトブリージングスペースでパーツを実行します。

function doLongCalculation(callbackFunction) {
  var partialResult = {};
  // part of the work, filling partialResult
  setTimeout(function(){ doSecondBit(partialResult, callbackFunction); }, 10);
}

function doSecondBit(partialResult, callbackFunction) {
  // more 'part of the work', filling partialResult
  setTimeout(function(){ finishUp(partialResult, callbackFunction); }, 10);
}

function finishUp(partialResult, callbackFunction) {
  var result;
  // do last bits, forming final result
  callbackFunction(result);
}

長い計算は、ほとんどの場合、いくつかのステップを実行しているため、または同じ計算を100万回実行していて、バッチに分割できるため、いくつかのステップにリファクタリングできます。これを(誇張して)持っている場合:

var resuls = [];
for(var i=0; i<1000000; i++) {
  // computation is performed here
  if(...) results.Push(...);
}

その後、コールバックを使用してこれをタイムアウト緩和された関数に簡単にカットできます

function runBatch(start, end, terminal, results, callback) {
  var i;
  for(var i=start; i<end; i++) {
    // computation is performed here
    if(...) results.Push(...);      } 
  if(i>=terminal) {
    callback(results);
  } else {
    var inc = end-start;
    setTimeout(function() {
      runBatch(start+inc, end+inc, terminal, results, callback);
    },10);
  }
}

function dealWithResults(results) {
  ...
}

function doLongComputation() {
  runBatch(0,1000,1000000,[],dealWithResults);
}

TL; DR:長い計算を実行しないでください。ただし、必要な場合は、サーバーに代わりに非同期のAJAX呼び出し。サーバーは処理を高速化でき、ページはブロックされません。

クライアントでJSの長い計算を処理する方法のJSの例は、AJAX呼び出しを実行するオプションがない場合にこの問題に対処する方法を説明するためだけにあります。そうではありません。

edit

また、賞金の説明は The XY problem の典型的なケースであることにも注意してください。

2019編集

最新のJSでは、タイムアウト/コールバックのawait/asyncコンセプトが大幅に改善されているため、代わりにそれらを使用してください。任意のawaitは、スケジュールされた更新を安全に実行できることをブラウザーに通知するため、「同期のように構造化された」方法でコードを記述しますが、関数をasyncとしてマークしてから、 awaitそれらを呼び出すたびに、それらを出力します。

async doLongCalculation() {
  let firstbit = await doFirstBit();
  let secondbit = await doSecondBit(firstbit);
  let result = await finishUp(secondbit);
  return result;
}

async doFirstBit() {
  //...
}

async doSecondBit...

...

イベントとタイミングの詳細 (ちなみに興味深い読み物)の「スクリプトに時間がかかりすぎて重いジョブがかかる」セクションで説明されているように、

[...]ジョブをパーツに分割し、それらを順番にスケジュールします。 [...]次に、ブラウザがパーツ間で応答するための「自由な時間」があります。それは他のイベントをレンダリングして反応することができます。訪問者もブラウザも満足しています。

タスクを小さなタスクやフラグメントに分割できないことが何度もあると思います。しかし、これが可能になる他の多くの時があると私は確信しています! :-)

提供されている例では、いくつかのリファクタリングが必要です。あなたはあなたがしなければならない仕事の一部を行うための関数を作成することができます。それはこのように始まるかもしれません:

function doHeavyWork(start) {
    var total = 1000000000;
    var fragment = 1000000;
    var end = start + fragment;

    // Do heavy work
    for (var i = start; i < end; i++) {
        //
    }

作業が終了すると、関数は次のワークピースを実行する必要があるかどうか、または実行が終了したかどうかを判断する必要があります。

    if (end == total) {
        // If we reached the end, stop and change status
        document.getElementById("btn").innerHTML = "done!";
    } else {
        // Otherwise, process next fragment
        setTimeout(function() {
            doHeavyWork(end);
        }, 0);
    }            
}

主なdowork()関数は次のようになります。

function dowork() {
    // Set "working" status
    document.getElementById("btn").innerHTML = "working";

    // Start heavy process
    doHeavyWork(0);
}

http://jsfiddle.net/WsmUh/19/ で完全に機能するコード(Firefoxでは穏やかに動作するようです)。

3
German Latorre

setTimeoutを使用したくない場合は、WebWorkerのままにします。ただし、HTML5対応のブラウザーが必要になります。

これはあなたがそれらを使うことができる一つの方法です-

HTMLとインラインスクリプトを定義します(インラインスクリプトを使用する必要はありません。既存の個別のJSファイルにURLを指定することもできます)。

<input id="start" type="button" value="Start" />
<div id="status">Preparing worker...</div>

<script type="javascript/worker">
    postMessage('Worker is ready...');

    onmessage = function(e) {
        if (e.data === 'start') {
            //simulate heavy work..
            var max = 500000000;
            for (var i = 0; i < max; i++) {
                if ((i % 100000) === 0) postMessage('Progress: ' + (i / max * 100).toFixed(0) + '%');
            }
            postMessage('Done!');
        }
    };
</script>

インラインスクリプトの場合、タイプjavascript/workerでマークします。

通常のJavaScriptファイル-

インラインスクリプトをWebWorkerに渡すことができるBlob-urlに変換する関数。これはIEでの作業に注意する可能性があり、通常のファイルを使用する必要があります:

function getInlineJS() {
    var js = document.querySelector('[type="javascript/worker"]').textContent;
    var blob = new Blob([js], {
        "type": "text\/plain"
    });
    return URL.createObjectURL(blob);
}

セットアップワーカー:

var ww = new Worker(getInlineJS());

WebWorkerからメッセージ(またはコマンド)を受信します。

ww.onmessage = function (e) {
    var msg = e.data;

    document.getElementById('status').innerHTML = msg;

    if (msg === 'Done!') {
        alert('Next');    
    }
};

このデモでは、ボタンをクリックして開始します。

document.getElementById('start').addEventListener('click', start, false);

function start() {
    ww.postMessage('start');
}

ここでの作業例:
http://jsfiddle.net/AbdiasSoftware/Ls4XJ/

ご覧のとおり、ワーカーでビジーループを使用している場合でも、ユーザーインターフェイスが更新されます(この例では進行状況が表示されます)。これは、Atomベースの(遅い)コンピュータでテストされました。

WebWorkerを使用したくない、または使用できない場合は、haveを使用してsetTimeoutを使用します。

これは、イベントをキューに入れることができる唯一の方法(setInterval以外)であるためです。お気づきのように、UIエンジンにいわば「余裕のある」UIエンジンを提供するため、数ミリ秒かかる必要があります。 JSはシングルスレッドなので、他の方法でイベントをキューに入れることはできません(requestAnimationFrameは、このコンテキストではうまく機能しません)。

お役に立てれば。

2
user1693593

更新:長期的には、タイムアウトを使用せずに、Firefoxが積極的にDOM更新を回避することを確実に回避できるとは思いません。再描画/ DOMの更新を強制したい場合は、要素のオフセットを調整したり、hide()を実行した後にshow()を実行したりするなど、利用可能なトリックがありますが、しばらくすると、それほどうまく利用できるものはありません。トリックが悪用されてユーザーエクスペリエンスが低下すると、ブラウザーはそれらのトリックを無視するように更新されます。いくつかの例については、この記事とその横にあるリンクされた記事を参照してください。 Chrome/MacでDOMを再描画/更新する

他の回答は必要な基本要素があるように見えますが、私の慣習は、事実を回避するために必要な一時停止を処理する「ディスパッチ」関数ですべてのインタラクティブなDOM変更機能をラップすることであることを言及する価値があると思いましたFirefoxはDOMの更新を回避するために非常に積極的であり、ベンチマークで適切なスコアを獲得します(インターネットを閲覧しているときにユーザーに応答します)。

私はあなたのJSFiddleを見て、私のプログラムの多くが依存しているディスパッチ機能をカスタマイズしました。私はそれは自明であると思います、そしてあなたはそれをあなたの既存のJSに貼り付けることができますFiddleそれがどのように機能するかを見る:

$("#btn").on("click", function() { dispatch(this, dowork, 'working', 'done!'); });

function dispatch(me, work, working, done) {
    /* work function, working message HTML string, done message HTML string */
    /* only designed for a <button></button> element */
    var pause = 50, old;
    if (!me || me.tagName.toLowerCase() != 'button' || me.innerHTML == working) return;
    old = me.innerHTML;
    me.innerHTML = working;
    setTimeout(function() {
        work();
        me.innerHTML = done;
        setTimeout(function() { me.innerHTML = old; }, 1500);
    }, pause);
}

function dowork() {
        for (var i = 1; i<1000000000; i++) {
            //
        }

}

注:複数のクリックによるステータスの更新が同時に発生している場合、ユーザーを深刻に混乱させる可能性があるため、ディスパッチ機能は、同時に発生する呼び出しをブロックします。

1
Joseph Myers

2019以降、setTimeoutを使用して競合状態を作成する代わりに、フレームをスキップするためにdoublerequesAnimationFrameを使用しています。

function doRun() {
    document.getElementById('app').innerHTML = 'Processing JS...';
    requestAnimationFrame(() =>
    requestAnimationFrame(function(){
         //blocks render
         confirm('Heavy load done') 
         document.getElementById('app').innerHTML = 'Processing JS... done';
    }))
}
doRun()
<div id="app"></div>

使用例として、無限ループでモンテカルロを使用してpiを計算するとします。

forループを使用してモックwhile(true)をループする-これにより、ページ

function* piMonteCarlo(r = 5, yield_cycle = 10000){

  let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
  
  while(true){
  
     total++;
     if(total === Number.MAX_SAFE_INTEGER){
      break;
     }
     x = Math.random() * r * 2 - r;
     y = Math.random() * r * 2 - r;
     
     (Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
     
     if(total % yield_cycle === 0){
      yield 4 * hits / total
     }
  }
}

let pi_gen = piMonteCarlo(5, 1000), pi = 3;

for(let i = 0; i < 1000; i++){
// mocks
// while(true){
// basically last value will be rendered only
  pi = pi_gen.next().value
  console.log(pi)
  document.getElementById('app').innerHTML = "PI: " + pi
}
<div id="app"></div>

そして今度はrequestAnimationFrameを更新の間に使用することを考えてください;)

function* piMonteCarlo(r = 5, yield_cycle = 10000){

  let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
  
  while(true){
  
     total++;
     if(total === Number.MAX_SAFE_INTEGER){
      break;
     }
     x = Math.random() * r * 2 - r;
     y = Math.random() * r * 2 - r;
     
     (Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
     
     if(total % yield_cycle === 0){
      yield 4 * hits / total
     }
  }
}

let pi_gen = piMonteCarlo(5, 1000), pi = 3;


function rAFLoop(calculate){

  return new Promise(resolve => {
    requestAnimationFrame( () => {
    requestAnimationFrame(() => {
      typeof calculate === "function" && calculate()
      resolve()
    })
    })
  })
}
let stopped = false
async function piDOM(){
  while(stopped==false){
    await rAFLoop(() => {
      pi = pi_gen.next().value
      console.log(pi)
      document.getElementById('app').innerHTML = "PI: " + pi
    })
  }
}
function stop(){
  stopped = true;
}
function start(){
  if(stopped){
    stopped = false
    piDOM()
  }
}
piDOM()
<div id="app"></div>
<button onclick="stop()">Stop</button>
<button onclick="start()">start</button>
1
Estradiaz

Jsfiddleでコードを少し変更して、

$("#btn").on("click", dowork);

function dowork() {
    document.getElementById("btn").innerHTML = "working";
    setTimeout(function() {
        for (var i = 1; i<1000000000; i++) {
            //
        }
        document.getElementById("btn").innerHTML = "done!";
    }, 100);
}

タイムアウトをより適切な値に設定100msトリックをしてくれました。それを試してみてください。待ち時間を調整して、最適な値を見つけてください。

0
ElmoVanKielmo

DOMバッファーはAndroidのデフォルトブラウザーにも存在し、長時間実行されるJavaScriptはDOMバッファーを一度だけフラッシュします。それを解決するには、setTimeout(...、50)を使用します。

0
diyism

Ajaxリクエストを偽造する

function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
$.ajax({
    url: "/",
    complete: function () {
        //long running task here
        document.getElementById("myspan").innerHTML = "done";
    }
});}
0
TeamEASI.com

一部のブラウザは、onclick html属性を適切に処理しません。 jsオブジェクトでそのイベントを使用することをお勧めします。このような:

<button id="mybutt" class="buttonEnabled">
    <span id="myspan">do some work</span>
</button>

<script type="text/javascript">

window.onload = function(){ 

    butt = document.getElementById("mybutt");
    span = document.getElementById("myspan");   

    butt.onclick = function () {
        span.innerHTML = "doing some work";
        butt.disabled = true;
        butt.className = "buttonDisabled";

        //long running task here

        span.innerHTML = "done";
    };

};

</script>

私は実際の例でフィドルを作りました http://jsfiddle.net/BZWbH/2/

0
Goran Lepur

「onmousedown」にリスナーを追加して、長期実行関数のボタンテキストとクリックイベントを変更してみましたか?.

0
user1390282

これを試して

function longRunningTask(){
    // Do the task here
    document.getElementById("mybutt").value = "done";
}

function longrunningfunction() {
    document.getElementById("mybutt").value = "doing some work";

    setTimeout(function() {
        longRunningTask();
    }, 1);    
}
0
Jay Patel