0

I spent hours reading posts all over the web about controlling execution order in Javascript/jQuery, but I still don't understand.

I'm trying to write code that shows the execution progress of behind-the-scenes javascript on a webpage. I have a jsfiddle (http://jsfiddle.net/wlandau/9sbu9myb/) that accomplishes this using timeouts. The javascript is

var showProgress = function(x){
    $(".progress").hide();
    $(x).show();   
}

var step1 = function(){
    showProgress(".running");
};

var step2 = function(){
    for(var i = 0; i < 1000000000; ++i) 
        i = i + 1; 
};

var step3 = function(){
    showProgress(".done");
};

$(".begin").click(function(){
    step1();
    setTimeout(step2, 0);
    setTimeout(step3, 0);
});

$(".clear").click(function(){
    $(".progress").hide();   
});

I will eventually need to use ajax and php, and I've read that the recommended tools are when/then, Deferred objects, and promises. So I tried (http://jsfiddle.net/wlandau/an8moww6/), but the execution order is wrong. (The html page doesn't say "Running..." when the for loop is running.) That fiddle's javascript is

var showProgress = function(x){
    $(".progress").hide();
    $(x).show();   
}

var step1 = function(){
    var def = $.Deferred(); 
    showProgress(".running");
    return def.promise();
};

var step2 = function(){
    var def = $.Deferred(); 
    for(var i = 0; i < 1000000000; ++i) 
        i = i + 1; 
    return def.promise();
};

var step3 = function(){
    var def = $.Deferred(); 
    showProgress(".done");
    return def.promise();
};

var success = function(){
    console.log("success")   
};

var failure = function(){
    console.log("failure")   
};

$(".begin").click(function(){
    $.when(step1(), step2(), step3()).then(success, failure);
});

$(".clear").click(function(){
    $(".progress").hide();   
});

QUESTION 1 (first fiddle): When I change "step1()" to "setTimeout(step1, 0)" in the "$(".begin").click(function(){..}" block, the execution order breaks down. Why is this?

QUESTION 2 (second fiddle): Why is the execution order wrong when I try to use when/then/Deferred/promise?

QUESTION 3 (second fiddle): Why isn't the "success" function executing? I told it to print to the console, but the Chrome's Javascript console shows nothing.

landau
  • 5,636
  • 1
  • 22
  • 50
  • Question1: Does this refer to snippet 1 or 2 (with or without deferreds)? Can you be more specific about "breaks down", what did you observe? Question2: What exactly is "wrong"? What happens in an unexpected order? – Bergi Aug 18 '14 at 16:41
  • Question 1 refers to snippet 1, without the deferreds. The correct execution order is this: the user presses the "Begin" button, a box that says "Running..." appears, a big long for loop happens, and then the box with "Running..." is replaced with a box that says "Done.". In the situations I describe in questions 1 and 2, the "Running..." box doesn't appear long enough to actually see. I think it appears just after the for loop and is replaced in an instant with the "Done." box. I think the jsfiddles are clearer than this explanation. – landau Aug 18 '14 at 17:07
  • So the *order* is always correct, but not the *timing*? "Unfortunately", the first fiddle works fine for me even with the `setTimeout` swapped in, what browser are you using to experience this? – Bergi Aug 18 '14 at 17:12
  • I'm using Chrome (v36.0.1985.143). What's the difference between execution order and timing? – landau Aug 18 '14 at 17:55
  • Order means executing steps in the correct sequence (1-2-3 instead of 2-3-1), while by timing I referred to the (temporal) spacing between the steps (1-2-3 instead of 12----3). – Bergi Aug 18 '14 at 18:00
  • Ah, okay. Then I'm trying to talk about execution order. Either 1-2-3 or 12----3 is great, but I don't want 2-3-1. – landau Aug 18 '14 at 18:14
  • But the execution order isn't wrong, is it? You always end up with `Done`, not with `Running`, and `Done` isn't shown before the loop has executed? If you put `console.log` statements in your functions, you would always see `step1 step2 step3` - even if the DOM manipulation might not be displayed on the screen. – Bergi Aug 18 '14 at 18:22

1 Answers1

1

Why is the timing wrong when I try to use when/then/Deferred/promise (second fiddle)?

Because you don't have used any setTimeouts, effectively executing everything synchronously. The browser won't re-render the screen until it has finished processing, and you will only see the Done appear in the end.

Using Promises/Deferreds doesn't make your code asynchronous! You have to take care of that yourself:

function step…(){
    var def = $.Deferred(); 
    setTimeout(function() {
        …
    }, 0);
    return def.promise();
};

Also, calling step1(), step2(), step3() at once (and then passing the promises to $.when to wait until all are resolved) will run the async functions in parallel, which is not what you want. To chain them, use

step1().then(step2).then(step3).then(success, failure);

Why isn't the "success" function executing (second fiddle)?

Because your promises are never resolved. When you have finished your asynchronous task, you will need to tell the deferred about that (and typically pass the result value). The promises you had in your code where just pending forever, never executing any handlers.

Fixed:

function step1() {
    var def = $.Deferred();
    setTimeout(function() {
        showProgress(".running");
        def.resolve();
    }, 0);
    return def.promise();
}

function step2() {
    var def = $.Deferred();
    setTimeout(function() {
       for(var i = 0; i < 1000000000; ++i) 
            i = i + 1;
        def.resolve();
    }, 0);
    return def.promise();
}

function step3() {
    var def = $.Deferred();
    setTimeout(function() {
       showProgress(".done");
        def.resolve();
    }, 0);
    return def.promise();
}

Since your functions are not inherently asynchronous, you also might choose to do the action right away and only put setTimeout(def.resolve, 0) and delay only the handlers; you also might do both.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I'll add to that the OP should remember to make calls to reject when the deferreds fail or the failure callbacks will never be called. – Mike Cheel Aug 18 '14 at 17:28
  • On Question 1 I have no evidence why it would not work with a third `setTimeout`, but wild guesses are a) the 0 timeout is too low (there's actually a minimum of 4ms) b) timeouts with the same delay are scheduled together - so that in certain browsers optimisation might jump in and not do a render in between, like in the synchronous case. – Bergi Aug 18 '14 at 17:30
  • @MikeCheel: Sure, "resolving" means both fulfilling and rejecting, whatever is appropriate. In this case they hardly would fail anyway :-) – Bergi Aug 18 '14 at 17:32
  • Since the OP didn't seem to know that resolve must be called I thought I would mention that reject must be called for failures. – Mike Cheel Aug 18 '14 at 17:33
  • Nope. "Synchronous" code just means that statements are executed right after each other, blocking until they're done (`showStart(); /* loop */; showEnd();`). That does block rendering as well, though, so we want "asynchronous", non-blocking code. Check http://ejohn.org/blog/how-javascript-timers-work/ and read about JavaScripts' *event loop* (don't have a good link at hand). Now, to prevent race conditions, we want to "sequentialize" our async code, ensuring proper order. We can nest callbacks, or we use promises. – Bergi Aug 18 '14 at 18:18
  • Let me see if I understand. Just executing `step1(); step2(); step3();` will make the pure Javascript in the steps happen sequentially, but because of synchronicity, all browser rendering is blocked and pushed to the end. (Weird!) To make browser rendering happen after each step, I want each step by itself to be asynchronous. To make these asynchronous steps happen sequentially, I use promises. – landau Aug 18 '14 at 19:10
  • Yes, that's basically it. You could use something else than promises, but they provide a very nice abstraction for asynchronous tasks :-) – Bergi Aug 18 '14 at 19:17
  • Awesome, thanks for all the help! I didn't think about browser rendering as separate from my js code. I'll go ahead and read up on the event loop and concurrency model. – landau Aug 18 '14 at 19:20
  • I just implemented this in my main program, and saving works like a DREAM now! I feel like I'm in complete control. A couple more things, though. 1: I accidentally deleted a previous comment on this answer (just after Mike Cheel's last post). Sorry about that. I hope I learn to avoid those mistakes as I get more familiar with this forum. 2: Bergi, for readers who may copy and paste your code, would you change your three `setTimout`s to `setTimeout`? Just a minor spelling issue. – landau Aug 19 '14 at 00:53