Thursday, March 15, 2012

many endpoints, one function: it all depends on what this meaning of "this" is

So as a follow up to today's post, I made a new feature for our site: the MultiDataGrabber.

There are two ugly Patterns I've seen when you need to build a page but there's an ordering dependency that requires information from 2 or more endpoints to be acted on: personally I'm guilty of making long, ugly function chains, storing data as we go, and then the final link in the chain does all the work. The other anti-pattern is to have a single function with 2 synchronous function, so the data can be returned. That's easy to read but causes the site to stall as the synchronous call completes.

I suggested a third ugly pattern: call the 2 endpoints asynchronously, and have each callback store its data in the parent object and check if the other callback has done likewise; if not, return, if so, call the function that uses the data. But at work Ben suggested this could be generalized: it would be nice if we could specify multiple AJAX URLs and then have a function that is called when they have all completed.

Thus, the MultiDataGrabber was born. It's called like this:

var mdg = new MultiDataGrabber(["/rest/user/location",
"/rest/ecommerce/account",
"/rest/user/style"], allDone);



function allDone(ress){
ccdebug("res 0:"+inspect(ress[0]));
ccdebug("res 1:"+inspect(ress[1]));
ccdebug("res 2:"+inspect(ress[2]));
}


Pretty easy, huh? Ben also mentioned if you were having to do too much of this (rather than, say, be able to build a page in parallel) it might mean you had a mismatch between what the endpoints were returning and the function of the page, so you might ask for a refactoring of the former.

The implementation is short, and has a point I want to discuss, so here it is:
function MultiDataGrabber(urls,callback) {
this._countNeeded = urls.length;
this._countHave = 0;
this._finalCallback = callback;
this.ress = [];
//go through each url, make a helper object to store the context
for(var i = 0; i < this._countNeeded; i++){
var url = urls[i];
var helper = new MultiDataGrabber_helper(i,url,this);
global_databroker.getJson(url,CreateDelegate(helper,function(res){ 
this.funk(res);
/*weird(this)*/
}));
helper = undefined;
}
this._myCallback = function(spot, res){
this.ress[spot] = res;
this._countHave++;
if(this._countHave >= this._countNeeded){
this._finalCallback(this.ress);
}
}
}

function MultiDataGrabber_helper(pid,purl,pmdg){
this.id = pid;
this.url = purl;
this.mdg = pmdg;
this.funk = function(res){
this.mdg._myCallback(this.id,res);
}
}

/*

function weird(what){

ccdebug(what);

ccdebug(this);
}
*/
So this code looks pretty obvious... as I read through it I think "aha, OF COURSE I have to make a helper object to store the state during the callback, and OF COURSE it needs a CreateDelegate so the scope doesn't get stomped" but when I was writing it I ran into enough dead ends that (as I've admitted before) I had to acknowledge I am not 100% fluent in Javascript Scoping rules. And I left a fossil of that with the commented-out "weird" function. The function is passed a variable "what", and prints out that, and then it prints out "this".  When the code is run, it first prints the MultiDataGrabber_helper (since it has the CreateDelegate saying that that is the scope), and then prints the window object. But that seems odd to my Java-addled brain, because at invocation the weird() function was passed "this"... but it was a different "this" then you get when you're in the function itself.

Sometimes it alarms me that you can do so much in JavaScript without having this stuff down cold. If I had the same klutziness with Java classes, I'd never get anything done! (On the other hand, I had a cold bath awakening to Java's anonymous function callbacks when I had to use the "Wicket" framework. Apparently they are very familiar to people who used AWT and Swing and what not back in the day, and now I'm very comfortable with them, but then they seemed really ugly and strange.)


UPDATE: I made a Google Docs Presentation for a Boston jQuery meetup's 5 minute talk on this.


UPDATE TO THE UPDATE: At the meetup, Ben "Cowboy" Alman pointed out that functionality along these same lines was added to jQuery 1.5: they're called "deferreds", and defer (ha) to Eric Hynds' explanation of them. To be fair, I'm not sure if it would be easy to utilize them with the plumbing my company uses for AJAX (i.e. the databroker caching and the way we listen in to the traffic to know if we should stop polling for notifications) so I don't feel too too bad, but still it's nice to add this knowledge to my repertoire. 

No comments:

Post a Comment