27

クリックすると長時間実行される機能を実行するボタンがあります。関数の実行中にボタンのテキストを変更したいのですが、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 の適用による) が、テキストは変わりません。

動作させるには、時間を 0ms ではなく 50ms に設定する必要があります (ボタンのテキストを変更します)。今では、少なくともこれはばかげていると思います。0 ミリ秒の遅延で動作するかどうかは理解できますが、低速のコンピューターではどうなるでしょうか? 多分Firefoxはsettimeoutで100ミリ秒を必要とするでしょうか?それはかなりばかげているように聞こえます。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 を強制的に更新するための信頼性の高いクロスブラウザー メソッドを探しています (実行時間の長いコードの種類、ブラウザー、コンピューターの速度、および setTimeout に応じて 50 から 100 ミリ秒の範囲が必要になるため、必要な時間間隔が異なるため、推奨されるソリューションです)状況により)

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

4

12 に答える 12

25

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

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

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

難しいことを計算する必要がある場合は、AJAX 操作を使用してサーバーにオフロードします (ブラウザー間で普遍的であり、多くの場合、a) その操作の処理が速くなり、b) 非同期で待機しないことができる 30 秒の時間が得られます。結果が返される) または webworker バックグラウンド スレッドを使用する (ユニバーサルではありません)。

計算に時間がかかるが、それほど馬鹿げているわけではない場合は、コードをリファクタリングして、タイムアウトの呼吸スペースでパーツを実行できるようにします。

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 呼び出しを実行するオプションがない場合に、この問題をどのように処理するかを説明するためのものです。場合。

編集

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

2019年編集

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

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

async doFirstBit() {
  //...
}

async doSecondBit...

...
于 2013-06-08T13:33:31.993 に答える
8

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

Chrome 27.0.1453、Firefox 21.0、インターネット 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');
}

デモはこちら!

于 2013-06-14T07:47:50.597 に答える
5

2019 年現在、setTimeout を使用して競合状態を作成する代わりに、doubleを使用してフレームをスキップしています。 requesAnimationFrame

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そして、その間に for updates を使用することを考えてみてください ;)

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>

于 2019-08-26T14:05:25.820 に答える
3

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

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

タスクをより小さなタスクまたはフラグメントに分割できないことが何度もあると確信しています。しかし、これが可能な場合は他にもたくさんあると確信しています。:-)

提供されている例では、いくつかのリファクタリングが必要です。しなければならない作業の一部を行う関数を作成できます。次のように開始できます。

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 では穏やかに動作するようです)。

于 2013-06-13T16:05:04.177 に答える
2

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

これは、それらを使用できる1つの方法です-

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>

インライン スクリプトの場合は、 type でマークし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を使用する必要がありますsetTimeout

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

お役に立てれば。

于 2013-06-13T04:41:33.550 に答える
1

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";
    }
});}
于 2016-09-20T23:07:47.273 に答える
1

更新: 長期的には、タイムアウトを使用せずに 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++) {
            //
        }

}

注: 複数のクリックによるステータスの更新が同時に行われると、ユーザーをひどく混乱させる可能性があるため、ディスパッチ機能は通話が同時に発生するのをブロックします。

于 2013-06-14T16:43:47.883 に答える
0

これを試して

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

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

    setTimeout(function() {
        longRunningTask();
    }, 1);    
}
于 2013-06-01T19:36:59.770 に答える
0

「onmousedown」にリスナーを追加して、長時間実行機能のボタンテキストとクリックイベントを変更しようとしましたか。

于 2013-06-14T06:38:50.643 に答える
0

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うまくいきました。それを試してみてください。レイテンシーを調整して、最適な値を見つけてください。

于 2013-06-14T14:39:31.343 に答える