TheMook
TheMook

Reputation: 1541

Chaining or grouping multiple JavaScript function calls where order of operation is critical at times

I have created a web api method that carries out calculations using a json object I post to the method. My understanding is that a jquery post is asynchronous? Assuming this is so, I'd like to be able to chain together multiple calls to the js function that invokes this api method because certain calls are order-critical.

There are 80 calls to this asynchronous api method in a row. I don't want to do:

asyncMethodCall1(myParams).then(asyncMethodCall2(myParams).then...)) 
...

as there is no need to chain them like this as none of them depend on the other and can all run simultaneously. However at the end I need to do some other calculations that DO depend on the previous results having finished.

Is there a way of doing a sort of group of calls followed by a "then"? Is it simply a case of having something like:

mySynchronousFunction(params).then(function() {...do other calcs});

function mySynchronousFunction(params) {
    asyncmethodcall1(myparams);
    asyncmethodcall2(myparams);
    asyncmethodcall3(myparams);
    ...
    asyncmethodcall76(myparams);
}

or do I need to have a "setTimeOut" in there somewhere? JavaScript is something I'm tryign to learn and it's all a bit odd to me at the moment!

EDIT 1

Well I'm stumped.

I know it's a fundamental lack of understanding on my part that's causing the problem but it's very hard to find basic introductory stuff that someone coming from a synchronous language can follow and understand. Currently a resource I'm working through is this one and it seems to make some sense but it's still not sinking in: http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt1-theory-and-semantics

Currently I have this:

        $.when(
            individualImpactCalc('byWeightAndFactor', 'CO2e', articleResults().material_CO2e),
            individualImpactCalc('byWeightAndFactor', 'Water', articleResults().material_Water),
            individualImpactCalc('byWeightAndFactor', 'pH', articleResults().material_pH),

        ...lots of other calls in here...

        ).then(function () {
            //do calculation totalling
            alert("I'm done!");
        }).fail(function() {
            alert("Argh! I failed!");
        });

...and it simply doesn't work as I want it to. I never get an alert showing. The impact calculations are done, the observables are updated, my page values change, but no alerts.

What am I fundamentally missing here?

Edit 2

What I was fundamentally missing was the difficulty in debugging chained promises! It actually was working once I cured a hidden reference error that wasn't bubbling up. Lots of painstaking stepping through javascript finally found it.

Just to add in another element to the mix of answers, I used the Q library as that was what is used in Durandal/Breeze anyway and it's just easy to keep the code consistent. I tried it with $.when and it worked just as well. I tried it with Promise and it failed with "Promise is not defined".

My working Q implementation:

        var calcPromise = Q.all([
            individualImpactCalc('byWeightAndFactor', 'CO2e', articleResults().material_CO2e),
            individualImpactCalc('byWeightAndFactor', 'Water', articleResults().material_Water),
            individualImpactCalc('byWeightAndFactor', 'pH', )]);

        return calcPromise
            .then(calcsDone)
            .fail(calcsFailed);

        function calcsDone() {
            alert("all calcs done");
        }
        function calcsFailed() {
            alert("Argh! Calc failure...");
        }

Upvotes: 0

Views: 1957

Answers (3)

djk
djk

Reputation: 973

As others have pointed out, you can use $.when(...) to call all of the methods at once and only continue with .then() when all are done. But that doesn't solve your problem, because the methods are started all at once and there is no chaining. Calls to your web api can still come in and finish in random order.

Even though your example shows different async methods, your description sounds like you have only one method with different parameter values. So why not collecting those parameters in an array and then chaining the calls in a loop?

var params = [ 27, 38, 46, 83, 29, 22 ];

var promise = asyncmethodcall(params[0]);
for (var i=1; i<params.length; i++) {
    promise = promise.then(buildAsyncmethodcall(params[i]));
}

function buildAsyncmethodcall(param) {
    return function() {
        return asyncmethodcall(param);
    }
}

http://jsfiddle.net/qKS5e/ -- see here why I had to build the function using another function

If you really want to call different methods, you could write a jQuery plugin eg. $.whenSequential(...) to which you pass an array of functions that return Deferred objects like this:

$.whenSequential( // your cool new plugin
    function() { return callasyncmethod1(123); },
    function() { return callasyncmethod2(345); },
    function() { return callasyncmethod3(678); }
);

The plugin method would work just like the for-loop above, but instead of params you iterate the function arguments and pass one by one to then().

Either way, you won't get around wrapping all calls in a function somehow to chain them, because otherwise they would get executed immediately (like with $.when).

Upvotes: 1

Tibos
Tibos

Reputation: 27833

What you are looking for is the jQuery when method: http://api.jquery.com/jQuery.when/

In the case where multiple Deferred objects are passed to jQuery.when, the method returns the Promise from a new "master" Deferred object that tracks the aggregate state of all the Deferreds it has been passed. The method will resolve its master Deferred as soon as all the Deferreds resolve, or reject the master Deferred as soon as one of the Deferreds is rejected.

Specifically you put the calls that don't depend on each other in when, and the ones that depend on them in the then.

$.when(/* independent calls */).then(/* dependent calls */);

So as an example if you want to run deffered 1 and 2 in paralel, then run 3, then run 4 you can do:

$.when(def1, def2).then(def3).then(def4);

Upvotes: 1

Barmar
Barmar

Reputation: 781878

You can use Promise.when() to wait for all of them.

function mySynchronousFunction(params) {
    return Promise.when(asyncmethodcall1(myparams),
                        asyncmethodcall2(myparams),
                        ...);
}

Promise.when returns a new promise, so you can chain this with .then().

See Asynchronous Programming in JavaScript with “Promises”

Upvotes: 1

Related Questions