First off, to chain a bunch of functions, you need them to be configured as methods on a common object. Then, each method can return that object so the return result is the object and the next method can be called on that object.
So, to do something like this:
a().b().c();
You need a()
to return an object that has the method b()
on it as a property. And, similarly you need a().b()
to return an object that has c()
on it as a property. This is generally called the Fluent Interface. That's all pretty straightforward by itself for synchronous methods. This is exactly how jQuery does its chaining.
$(".foo").show().css("color", "blue");
All three of these calls all return a jQuery object and that jQuery object contains all the methods that you can chain.
In the example above, you could do synchronous chaining like this:
function a() {
}
a.prototype = {
b: function() {
// do something
return this;
},
c: function() {
// do something else
return this;
}
};
But, your question is about asynchronous operations. That is significantly more work because when you do:
a().b().c();
That's going to execute all three methods immediately one after the other and will not wait for any of them to complete. With this exact syntax, the only way I know of to support chaining is to build a queue where instead of actually executing .b(xxx)
right away, the object queues that operation until a()
finishes. This is how jQuery does animations as in:
$(".foo").slideUp().slideDown();
So, the object that is returned from each method can contain a queue and when one operation completes, the object then pulls the next item from the queue, assigns it's arguments (that are also held in the queue), executes it and monitors for that async operation to be done where it again pulls the next item from the queue.
Here's a general idea for a queue. As I got into this implementation, I realized that promises would make this a lot easier. Here's the general idea for an implementation that doesn't use promises (untested):
For simplicity of example for async operations, lets make a()
execute a 10ms setTimeout, .b()
a 50ms setTimeout and .c()
a 100ms setTimeout. In practice, these could be any async operations that call a callback when done.
function a() {
if (!(this instanceof a)) {
return new a();
} else {
this.queue = [];
this.inProgress = false;
this.add(function(callback) {
// here our sample 10ms async operation
setTimeout(function() {
callback(null);
}, 10);
}, arguments);
}
}
a.prototype = {
b: function() {
this.add(function(callback) {
// here our sample 50ms async operation
setTimeout(function() {
callback(null);
}, 50);
return this;
}, arguments);
},
c: function(t) {
this.add(function(t, callback) {
// here our sample 100ms async operation
setTimeout(function() {
callback(null);
}, t);
return this;
}, arguments);
},
add: function(fn, args) {
// make copy of args
var savedArgs = Array.prototype.slice.call(args);
this.queue.push({fn: fn, args:savedArgs});
this._next();
},
_next: function() {
// execute the next item in the queue if one not already running
var item;
if (!this.inProgress && this.queue.length) {
this.inProgress = true;
item = this.queue.shift();
// add custom callback to end of args
item.args.push(function(err) {
this.inProgress = false;
if (err) {
// clear queue and stop execution on an error
this.queue = [];
} else {
// otherwise go to next queued operation
this._next();
}
});
try {
item.fn.apply(this, item.args);
} catch(e) {
// stop on error
this.queue = [];
this.inProgress = false;
}
}
}
};
// usage
a().b().c(100);
If we use promises for both our async operations and for the queuing, then things get a bit simpler:
All async operations such as firstAsyncOperation
and secondAsyncOperation
here return a promise which drastically simplifies things. The async chaining is done for us by the promise infrastructure.
function a(arg1, arg2) {
if (!(this instanceof a)) {
return new a(arg1, arg2);
} else {
this.p = firstAsyncOperation(arg1, arg2);
}
}
a.prototype = {
b: function() {
return this._chain(secondAsyncOperation, arguments);
},
c: function() {
return this._chain(thirdAsyncOperation, arguments);
},
_chain: function(fn, args) {
var savedArgs = Array.prototype.slice.call(args);
this.p = this.p.then(function() {
return fn.apply(this, savedArgs);
});
return this;
},
then: function(a, b) {
this.p = this.p.then(a, b);
return this;
},
catch: function(fn) {
this.p = this.p.catch(fn);
return this;
}
};
// usage:
a().b().c(100).then(function() {
// done here
}).catch(function() {
// error here
});