DOM refresh on long running function

后端 未结 12 2290
醉话见心
醉话见心 2020-11-27 20:54

I have a button which runs a long running function when it\'s clicked. Now, while the function is running, I want to change the button text, but I\'m having problems in some

相关标签:
12条回答
  • 2020-11-27 21:29

    DOM buffer also exists in default browser on android, long running javascript only flush DOM buffer once, use setTimeout(..., 50) to solve it.

    0 讨论(0)
  • 2020-11-27 21:37

    Webpages are updated based on a single thread controller, and half the browsers don't update the DOM or styling until your JS execution halts, giving computational control back to the browser. That means if you set some element.style.[...] = ... it won't kick in until your code finishes running (either completely, or because the browser sees you're doing something that lets it intercept processing for a few ms).

    You have two problems: 1) your button has a <span> in it. Remove that, just set .innerHTML on the button itself. But this isn't the real problem of course. 2) you're running very long operations, and you should think very hard about why, and after answering the why, how:

    If you're running a long computational job, cut it up into timeout callbacks (or, in 2019, await/async - see note at the end of this anser). Your examples don't show what your "long job" actually is (a spin loop doesn't count) but you have several options depending on the browsers you take, with one GIANT booknote: don't run long jobs in JavaScript, period. JavaScript is a single threaded environment by specification, so any operation you want to do should be able to complete in milliseconds. If it can't, you're literally doing something wrong.

    If you need to calculate difficult things, offload it to the server with an AJAX operation (universal across browsers, often giving you a) faster processing for that operation and b) a good 30 seconds of time that you can asynchronously not-wait for the result to be returned) or use a webworker background thread (very much NOT universal).

    If your calculation takes long but not absurdly so, refactor your code so that you perform parts, with timeout breathing space:

    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);
    }
    

    A long calculation can almost always be refactored into several steps, either because you're performing several steps, or because you're running the same computation a million times, and can cut it up into batches. If you have (exaggerated) this:

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

    then you can trivially cut this up into a timeout-relaxed function with a callback

    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: don't run long computations, but if you have to, make the server do the work for you and just use an asynchronous AJAX call. The server can do the work faster, and your page won't block.

    The JS examples of how to deal with long computations in JS at the client are only here to explain how you might deal with this problem if you don't have the option to do AJAX calls, which 99.99% of the time will not be the case.

    edit

    also note that your bounty description is a classic case of The XY problem

    2019 edit

    In modern JS the await/async concept vastly improves upon timeout callbacks, so use those instead. Any await lets the browser know that it can safely run scheduled updates, so you write your code in a "structured as if it's synchronous" way, but you mark your functions as async, and then you await their output them whenever you call them:

    async doLongCalculation() {
      let firstbit = await doFirstBit();
      let secondbit = await doSecondBit(firstbit);
      let result = await finishUp(secondbit);
      return result;
    }
    
    async doFirstBit() {
      //...
    }
    
    async doSecondBit...
    
    ...
    
    0 讨论(0)
  • 2020-11-27 21:38

    If you don't want to use setTimeout then you are left with WebWorker - this will require HTML5 enabled browsers however.

    This is one way you can use them -

    Define your HTML and an inline script (you don't have to use inline script, you can just as well give an url to an existing separate JS file):

    <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>
    

    For the inline script we mark it with type javascript/worker.

    In the regular Javascript file -

    The function that converts the inline script to a Blob-url that can be passed to a WebWorker. Note that this might note work in IE and you will have to use a regular file:

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

    Setup worker:

    var ww = new Worker(getInlineJS());
    

    Receive messages (or commands) from the WebWorker:

    ww.onmessage = function (e) {
        var msg = e.data;
    
        document.getElementById('status').innerHTML = msg;
    
        if (msg === 'Done!') {
            alert('Next');    
        }
    };
    

    We kick off with a button-click in this demo:

    document.getElementById('start').addEventListener('click', start, false);
    
    function start() {
        ww.postMessage('start');
    }
    

    Working example here:
    http://jsfiddle.net/AbdiasSoftware/Ls4XJ/

    As you can see the user-interface is updated (with progress in this example) even if we're using a busy-loop on the worker. This was tested with an Atom based (slow) computer.

    If you don't want or can't use WebWorker you have to use setTimeout.

    This is because this is the only way (beside from setInterval) that allow you to queue up an event. As you noticed you will need to give it a few milliseconds as this will give the UI engine "room to breeth" so-to-speak. As JS is single-threaded you cannot queue up events other ways (requestAnimationFrame will not work well in this context).

    Hope this helps.

    0 讨论(0)
  • 2020-11-27 21:39

    Some browsers don't handle onclick html attribute good. It's better to use that event on js object. Like this:

    <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>
    

    I made a fiddle with working example http://jsfiddle.net/BZWbH/2/

    0 讨论(0)
  • 2020-11-27 21:42

    Update: I don't think in the long term that you can be sure of avoiding Firefox's aggressive avoidance of DOM updates without using a timeout. If you want to force a redraw / DOM update, there are tricks available, like adjusting the offset of elements, or doing hide() then show(), etc., but there is nothing very pretty available, and after a while when those tricks get abused and slow down user experience, then browsers get updated to ignore those tricks. See this article and the linked articles beside it for some examples: Force DOM redraw/refresh on Chrome/Mac

    The other answers look like they have the basic elements needed, but I thought it would be worthwhile to mention that my practice is to wrap all interactive DOM-changing functions in a "dispatch" function which handles the necessary pauses needed to get around the fact that Firefox is extremely aggressive in avoiding DOM updates in order to score well on benchmarks (and to be responsive to users while browsing the internet).

    I looked at your JSFiddle and customized a dispatch function the one that many of my programs rely on. I think it is self-explanatory, and you can just paste it into your existing JS Fiddle to see how it works:

    $("#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++) {
                //
            }
    
    }
    

    Note: the dispatching function also blocks calls from happening at the same time, because it can seriously confuse users if status updates from multiple clicks are happening together.

    0 讨论(0)
  • 2020-11-27 21:43

    As of 2019 one uses double requesAnimationFrame to skip a frame instead of creating a race condition using setTimeout.

    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>


    As an usage example think of calculating pi using Monte Carlo in an endless loop:

    using for loop to mock while(true) - as this breaks the page

    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>

    And now think about using requestAnimationFrame for updates in between ;)

    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>

    0 讨论(0)
提交回复
热议问题