jQuery/Javascript - How to wait for manipulated DOM to update before proceeding with function

30,950

Solution 1

set it to processing, then do a setTimeout to prevent the cpu intensive task from running until after the div has been updated.

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" ></script>
<script>
function addSecs(d, s) {return new Date(d.valueOf()+s*1000);}
function doRun() {
    document.getElementById('msg').innerHTML = 'Processing JS...';
    setTimeout(function(){
         start = new Date();
         end = addSecs(start,5);
         do {start = new Date();} while (end-start > 0);
         document.getElementById('msg').innerHTML = 'Finished Processing';   
    },10);
}
$(function() {
    $('button').click(doRun);
});    
</script>
    </head>
<body>
    <div id="msg">Not Started</div>
    <button>jQuery</button>
    <a href="#" onclick="doRun()">javascript</a>
</body>
</html>

you can modify the setTimeout delay as needed, it may need to be larger for slower machines/browsers.

Edit:

You could also use an alert or a confirm dialog to allow the page time to update.

document.getElementById('msg').innerHTML = 'Processing JS...';
if ( confirm( "This task may take several seconds. Do you wish to continue?" ) ) {
     // run code here
}

Solution 2

You have a loop that runs for 5 seconds and freezes the web browser during that time. Since the web browser is frozen it can't do any rendering. You should be using setTimeout() instead of a loop, but I'm assuming that loop is just a replacement for a CPU intensive function that takes a while? You can use setTimeout to give the browser a chance to render before executing your function:

jQuery:

$(function() {
    $('button').click(function(){
        (function(cont){
            $('div').text('Processing JQ...');  
            start = new Date();
            end = addSecs(start,5);
            setTimeout(cont, 1);
        })(function(){
            do {start = new Date();} while (end-start > 0);
            $('div').text('Finished JQ');   
        })
    });
});

Vanilla JS:

document.getElementsByTagName('a')[0].onclick = function(){
    doRun(function(){
         do {start = new Date();} while (end-start > 0);
         document.getElementById('msg').innerHTML = 'Finished JS';   
    });
    return false;
};

function doRun(cont){
    document.getElementById('msg').innerHTML = 'Processing JS...';
    start = new Date();
    end = addSecs(start,5);
    setTimeout(cont, 1);
}

You should also remember to always declare all variables using the var keyword, and avoid exposing them to the global scope. Here is a JSFiddle:

http://jsfiddle.net/Paulpro/ypQ6m/

Solution 3

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

....
function doRun() {
    document.getElementById('msg').innerHTML = 'Processing JS...';
    requestAnimationFrame(() =>
    requestAnimationFrame(function(){
         start = new Date();
         end = addSecs(start,5);
         do {start = new Date();} while (end-start > 0);
         document.getElementById('msg').innerHTML = 'Finished Processing';   
    }))
}
...

Solution 4

I had to wait for jQuery to manipulate the DOM and then grap the changes (load form fields multiple times into a form, then to submit it). The DOM grew and the insertion took longer and longer. I saved the changes using ajax to have to user to be able to continue where he left off.

This did NOT WORK as intended:

jQuery('.parentEl').prepend('<p>newContent</p>');
doSave(); // wrapping it into timeout was to short as well sometimes

Since jQuery functions like .prepend() do continue the chain only when done, the following seemed to do the trick:

jQuery('.parentEl').prepend('<p>newContent</p>').queue(function() {
  doSave();
});
Share:
30,950
pcormier
Author by

pcormier

Updated on August 27, 2020

Comments

  • pcormier
    pcormier almost 4 years

    What I'm trying to do is to update a simple div to say "Processing..." before executing a CPU-intensive script (it takes 3-12 seconds to run, no AJAX) then update the div to say "Finished!" when done.

    What I'm seeing is the div never gets updated with "Processing...". If I set a breakpoint immediately after that command, then the div text does get updated, so I know the syntax is correct. Same behavior in IE9, FF6, Chrome13.

    Even when bypassing jQuery and using basic raw Javascript, I see the same issue.

    You'd think this would have an easy answer. However, since the jQuery .html() and .text() don't have a callback hook, that's not an option. It's also not animated, so there is no .queue to manipulate.

    You can test this yourselves using the sample code I prepared below that shows both the jQuery and Javascript implementations with a 5 second high-CPU function. The code is easy to follow. When you click either the button or the link, you never see "Processing..."

    <!DOCTYPE html>
    <html>
    <head>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" ></script>
    <script type="text/javascript">
    function addSecs(d, s) {return new Date(d.valueOf()+s*1000);}
    function doRun() {
        document.getElementById('msg').innerHTML = 'Processing JS...';
        start = new Date();
        end = addSecs(start,5);
        do {start = new Date();} while (end-start > 0);
        document.getElementById('msg').innerHTML = 'Finished JS';   
    }
    $(function() {
        $('button').click(function(){
            $('div').text('Processing JQ...');  
            start = new Date();
            end = addSecs(start,5);
            do {start = new Date();} while (end-start > 0);
            $('div').text('Finished JQ');   
        });
    });
    </script>
    </head>
    <body>
        <div id="msg">Not Started</div>
        <button>jQuery</button>
        <a href="#" onclick="doRun()">javascript</a>
    </body>
    </html>
    
  • pcormier
    pcormier almost 13 years
    Yes, the loop was simply meant to represent the CPU-intensive function. This was not intended to be a representation of production code, just a quick snippet to demonstrate the problem.
  • pcormier
    pcormier almost 13 years
    Great! The setTimeout permits the rendering to complete. Hopefully there won't be a case where it takes longer than 10ms to do so. (I suppose extending to 100ms wouldn't hurt.) Though the dialog solution fixes the problem as well, it adds an unnecessary and annoying extra click.
  • yerforkferchips
    yerforkferchips almost 9 years
    -1 because this is effectively a race condition. There's no guarantee the elapsed time will be lower than 10ms even on fast machines here...
  • Kevin B
    Kevin B almost 9 years
    @yerforkferchips Yes, it does guarentee it due to how the event loop works. All we have to do is push the callback into the callback queue so that it won't run until the next time the event loop runs. It could even have been as small as 0ms and still work everywhere because the renderer (which also gets put into the callback queue) has priority.
  • Joseph Rogers
    Joseph Rogers over 8 years
    I would like to appologise for my accidental downvote, I didn't even realise I'd done it until I spotted my rep had dropped by 1 point, by which time it was too late to change it :( DOH, especially since this answer actually helped me and I would normally have +1ed it.
  • HaulinOats
    HaulinOats about 7 years
    @KevinB I have no idea why this isn't more common knowledge (timeout can be 0ms and still work). I don't run into this issue that often, but when I do, I never fully understood why setting a timeout always fixed it other than assuming I had to set an arbitrary time to allow the DOM to finish updating. Thank you so much.
  • Kevin B
    Kevin B about 7 years
    @Brett84c the event loop can be a difficult concept to grasp, but once you do, javascript in general makes a whole lot more sense.
  • HaulinOats
    HaulinOats about 7 years
    @KevinB So upon further research I did find out that jQuery's .html() function is synchronous unless this answer isn't currently accurate. Would the timeout method your suggesting be more for vanilla Javascript DOM updates?
  • Kevin B
    Kevin B about 7 years
    @Brett84c the fact that it is synchronous doesn't change anything. It will in change the dom immediately, but until the callstack is clear the UI won't update. The ui won't update until after the long cpu intensive task.
  • HaulinOats
    HaulinOats about 7 years
    So in essence, if you're relying on the DOM for data attributes that are necessary for further processing in the current call stack, you're gonna have problems.
  • Kevin B
    Kevin B about 7 years
    well, no, dom attributes and properties update synchronously. they just don't render synchronously.
  • HaulinOats
    HaulinOats about 7 years
    Ok, i'm understanding better now. DOM will update synchronously in the callstack, just won't RENDER until it's done.
  • Kevin B
    Kevin B about 7 years
    Right. It's the same thing you can observe when you perform a dom update followed by an alert('foo'). The alert will pop up, then after you close the alert the dom update will render. This happens because the alert is synchronous, much like the cpu intensive task above.
  • TangMonk
    TangMonk over 4 years
    This made me confused
  • Estradiaz
    Estradiaz over 4 years
    @TangMonk can you say what is missing so i can update this- e.g. what confuses you?
  • Manny Alvarado
    Manny Alvarado about 3 years
    The good ol' settimeout. React was made to avoid this. Sadly I'm working on a jquery project and came over here looking for a good answer to this. Not possible without settimeout it seems.
  • Kevin B
    Kevin B about 3 years
    @MannyAlvarado Yep... though... often when this is needed, there's something going on that shouldn't be. Try thinking of other ways to accomplish it, particularly when working with react! Maybe you don't actually need the information from the dom to do what you're doing, or maybe it can be accomplished with CSS instead.