Could you, please, explain to me, how to write really basic flow control in JavaScript? Thank you.
flow([
function(callback) { /* do something */ callback(); /* run next function */ },
function(callback) { /* do something */ callback(); /* run next function */ },
function(callback) { /* do something */ callback(); /* run next function */ },
function(callback) { /* do something */ callback(); }
], function() {
alert("Done.");
});
Could you, please, explain to me, how to write really basic flow control in JavaScript? Thank you.
flow([
function(callback) { /* do something */ callback(); /* run next function */ },
function(callback) { /* do something */ callback(); /* run next function */ },
function(callback) { /* do something */ callback(); /* run next function */ },
function(callback) { /* do something */ callback(); }
], function() {
alert("Done.");
});
Share
Improve this question
edited Dec 21, 2010 at 1:46
Joey Adams
43.5k19 gold badges88 silver badges115 bronze badges
asked Dec 21, 2010 at 1:28
Josh NewtonJosh Newton
513 bronze badges
8
- 3 ...could you please explain in more detail what you want to do? – deceze ♦ Commented Dec 21, 2010 at 1:31
- I would like to write this flow control, but I don't know how. I browsed through lots of code, but I didn't get it. – Josh Newton Commented Dec 21, 2010 at 1:34
- Are the functions potentially asynchronous? – slebetman Commented Dec 21, 2010 at 1:42
- 1 Is this dude a friend of yours? stackoverflow./questions/4495110/flow-control-in-javascript/… – Pointy Commented Dec 21, 2010 at 1:45
- @Josh But you said below that you want them to run sequentially... – Šime Vidas Commented Dec 21, 2010 at 1:46
5 Answers
Reset to default 7Would something like this work?
function flow(fns, last) {
var f = last;
for (var i = fns.length - 1; i >= 0; i--)
f = makefunc(fns[i], f);
f();
}
function makefunc(f, g) {
return function() { f(g) }
}
I've been doing quite a bit of that in a recent project. I've written some code to help manage it. Here's the code. You pass the bundledAsync function an object with a "calls" parameter and a "bundleCallback" parameter. The calls parameter is an array of objets representing the function you want to call. In the fn param, you store a reference to the actual parameter. In the "args" param, you store your arguments. The last argument of each of the functions you pass in must be a callback, which must be called.
I'm miserable at documenting my code and making it useful to others, but this is really really useful to me. I suspect someone else has written something similar, perhaps properly documented. If you can't find it, and need help figuring this out, let me know.
/**
This is a way to submit multiple async calls, and be notified when they've all finished
<pre>
NameSpace.bundledAsync({
calls:[
{
fn: service.getGroups,
args: [
function(listsArg){
listsSummary = listsArg;
}
],
calls: function(){return UNAB.Util.makeArray(listsSummary, function(list){
return {
fn: service.getGroup,
args: [list.id, function(resp){listsDetail.push(resp)}]
}
})}
}
],
bundleCallback: function(){
callback(listsDetail)
}
});
</pre>
@class bundledAsync
@static
*/
NameSpace.bundledAsync = function(options){
var callbacksLeft = 0;
var calls = $.grep(options.calls, function(call){return call});
if(options.hasOwnProperty("bundleCallback") && typeof options.bundleCallback != "function"){
throw new Error("bundleCallback, passed to bundledAsync, must be a function.");
}
if(options.chain){ // if this is true, sibling calls will run in succession, not in parallel
calls.reverse();
var newCalls = [calls.pop()];
var lastCall = newCalls[0];
while(calls.length > 0){
if(lastCall.calls){
throw new Error("You can't nest calls if you're in chain mode");
}
lastCall.calls = [calls.pop()];
lastCall = lastCall.calls[0];
}
calls = newCalls;
}
var decrimentCallbacksLeft = function(){
if(options.name){
// log.debug("Starting decrimentCallbacksLeft for: " + options.name + ". Decrimenting callbacksLeft to: " + (callbacksLeft - 1));
}
if(--callbacksLeft == 0 && options.bundleCallback){
// log.debug("No callbacks left. Calling bundleCallback for name: " + options.name);
options.bundleCallback();
}
}
var doCalls = function(callsToDo){
if(typeof callsToDo == "function"){
callsToDo = callsToDo();
}else{
callsToDo = $.extend(true, [], callsToDo);// in case we want to reuse the calls
}
// right away, return if the calls are empty
// check to make sure callbacksLeft == 0, because
// we may be dealing with nested calls
if(callsToDo.length ==0 && callbacksLeft == 0){
// log.debug("callsToDo is empty, so call the callback right away.");
options.bundleCallback();
return null;
}
callbacksLeft += callsToDo.length;
$.each(callsToDo, function(index, call){
var numFns = 0;
// // Look through the args searching for functions.
// // When one is found, wrap it with our own function.
// // This assumes that each function has exactly one
// // callback, and that each callback is called exactly once
// args can be a function which will return the args,
// that way, you don't have to determine the args for the function until the moment it's called
call.args = call.jitArgs? call.args():call.args;
$.each(call.args, function(index, arg){
if(typeof arg === "function"){
numFns++;
// Here's where we wrap the original function's callback
call.args[index] = function(){
// when we get to this point, we know that the original function has totally pleted,
// and we can call any functions chained to this one, or finish the whole process
arg.apply(null, arguments); // call the original callback
if(call.calls){
// maybe we don't want to create the child calls until after
// the parent has returned. In that case, pass a function instead of an array
if(typeof call.calls === "function"){
call.calls = call.calls();
}
// if this call has any call of its own, send those out now
doCalls(call.calls);
}
decrimentCallbacksLeft();
}
}
});
if(numFns!=1){
throw new Error("Each function passed to bundledAsync must have one and only one arg which is a function");
}
// if(call.fn.length != call.args.length){
// log.warn("The current function is being called with a different number of arguments that that with which it was declared. Should be: "+call.fn.length+", was: "+call.args.length+" \n" + call.fn.toString());
// }
call.fn.apply(null, call.args);
});
}
doCalls(calls);
}
I remend reading about continuation-passing style. It seems your goal is, given an array of functions which take a continuation argument, chain them together such that the continuation proceeds to the next function in the array.
Here is an implementation of such a function:
function flow(funcArr, funcDone) {
function proceed(i) {
if (i < funcArr.length) {
return function() {
funcArr[i](proceed(i+1));
}
} else {
return funcDone;
}
}
proceed(0)();
}
Edit: Anon.'s answer is shorter and simpler.
Here's how it works: proceed(i)
returns a callback which calls the ith function (or, funcDone
, if none are left in the array). Because proceed(i)
returns a callback rather than doing it, we can use proceed(i+1)
as the contiunation function.
Sample usage:
flow([
function(cb) { print("Thing one"); cb(); },
function(cb) { print("Thing two"); cb(); },
function(cb) { print("Thing three"); cb(); },
function(cb) { print("Thing four"); cb(); },
], function() {
print("Done.");
});
Now try removing one of the cb();
calls. It will break the chain, which is probably exactly what you want. Another cool thing is, you can take cb
, shove it in a global variable, and call it later on to resume the flow.
Bear in mind there is a drawback to this approach: many (if not all) JavaScript interpreters don't optimize tail recursion. If the funcArr
is too long, you may get a stack overflow. This is a cross borne by any use of continuation-passing style in JavaScript.
(function(){
function a(cb) { alert('hi'); cb(); }
function b(cb) { alert('there'); cb(); }
function c(cb) { alert('replace alert with console.log for ease'); cb(); }
var done = function() { alert('done'); }
a(b(c(done)));
})()
// callback is a global function, I assume
function flow(funcArr, funcEnd) {
for (var i = 0; i < funcArr.length; i++) {
funcArr[i](callback);
}
funcEnd();
}
This would run all those functions.