Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

JavaScript for C# developers: callbacks (part II)

DZone's Guide to

JavaScript for C# developers: callbacks (part II)

· Web Dev Zone
Free Resource

Tips, tricks and tools for creating your own data-driven app, brought to you in partnership with Qlik.

Last time, we wrote a map method for arrays (which I had to call mapp, so that we didn’t clash with the native version present in many browsers). To use the map method, you have to provide a callback function that would be called for every element in the array.

The first change I want to make today is to allow for missing elements in the array. In that case, the callback shouldn’t be called. Here’s the obvious way of implementing this change:

Array.prototype.mapp = function (process, context) {
var i,
result = [],
last = this.length;
for (i = 0; i < last; i++) {
if (this[i]) {
result[i] = process.call(context, this[i], i);
}
}
return result;
};

var myArray = [2, 3, 5, 7, 11, 13];
myArray[7] = 19; // missing out myArray[6]
console.log(myArray);

var newArray = myArray.mapp(function (element, index) {
return "<" + index.toString() + ": " + element.toString() + ">";
});
console.log(newArray);

 

Since the seventh element is missing the expression this[6] will resolve as false and the if block won’t be executed. Unfortunately there are five other values that an existing element could have that will also evaluate as false: false, 0, NaN, the empty string, and null, all of which might appear in a real array. So this first cut just won’t work.

Another way is to expressly compare to undefined, like this:

 if (this[i] !== undefined) {
result[i] = process.call(context, this[i], i);
}

This works with the usual caveat that someone could have set undefined to some actual value. In which case we could use (void 0) instead (the void operator returns undefined for any operand), or we could enter (typeof this[i] !== "undefined") as an alternative.

Yuk, how about some proper JavaScript idiom? We’ll use the in keyword, it’s not just for the for..in statement:

    if (i in this) {
result[i] = process.call(context, this[i], i);
}

There. Much more legible.

Now we’ve got that sorted out, suppose that the array we’re mapping is large or that the processing for each element of the array is lengthy. We could be in danger of triggering the browser’s “script is taking too long” warning if we just blithely used the mapp method no matter what. What should we do?

Dominoesphoto © 2005 Jason | more info (via: Wylio)Let’s explore how we can split up the work done by the mapp method. First, though, it behooves us to understand why the browser might put up a warning dialog saying a script is taking too long. In essence, all of the JavaScript code on a page executes in one thread. Indeed JavaScript doesn’t have any way of spinning off other threads to do work. Since the one and only thread is also the UI thread, it means that a long-running piece of code would freeze the entire UI of the page. Not a good experience at all, which is why the browsers have a monitor to check that events are still flowing through the message pump. If a piece of code takes too long, the events are no longer being processed and after a given length of time, the browser interrupts the interpreter and puts up the warning dialog.

So, given all that, how can we split up our code so that the message pump still gets processing time? The answer is to use setTimeout. What this function does is to set a function executing after a certain length of time has passed. You pass both the function to execute (it’s a callback, of course) and the time to wait to setTimeout. What happens under the hood is that these requests are queued and some process pushes the callback onto the message loop after the timeout period has expired. At which point, of course, the function executes. (The same kind of thing happens with AJAX calls: when the AJAX call returns, it pushes the callback onto the message loop to get executed.)

What we would like to do is to split up our processing into “chunks”, each of which won’t take long to execute at all. After each chunk completes it queues up the next chunk to execute using setTimeout. However, the timeout used is going to be very short; not a large time like a second, but in the order of a few milliseconds at most. Essentially we are executing the chunks asynchronously rather than sequentially or synchronously. In between each there’s a bit of breathing room for the message loop to do other stuff.

Here’s a new function that will help us.

Function.prototype.delay = function () {
setTimeout(this, 10);
};


It’s defined as a method on the function prototype, so it’s available to all functions. All it does is to delay the execution of the function it’s called on by 10 milliseconds. Here’s a silly example of it in action:

var o = {
count: 10,
tick: function () {
if (o.count--) {
console.log("*");
o.tick.delay();
}
}
};

o.tick();

 

What we have here is an object with a count property initially set to 10, and a method called tick. This method outputs an asterisk to the console and then it calls itself using the delay method we just wrote, delaying for 10 milliseconds. It does this 10 times by decrementing count down to zero. If you run this, you’ll get 10 asterisks one after the other really quickly. Nice.

Note though that the initial call to o.tick returns immediately. If you ran it in Firebug you would actually get this result:

*
undefined
*
*
*
*
*
*
*
*
*

What’s that “undefined” doing in there? It’s Firebug telling us that the initial call to o.tick has completed and it returned undefined. Then we get the asterisks from the setTimeout followed by setTimeout, followed by setTimeout, etc, etc, each one firing off the other. The point I want to get across is that the initial function call completes before the delays fire. Delaying like this means that we have to be careful and not assume that all of the work we’re trying to do is completed straightaway. If we are concerned about knowing when a chained asynchronous process like this completes, we shall have to provide a completion callback so it can be fired when all’s done.

Back to the next version of our mapp function. We’ll create a new map function, call it mapAsync, that will process the array in chunks. To make it easy to begin with, we’ll make each chunk big enough to process a single element. First of all, we shall have to add a new parameter: the completion callback, as I mentioned above (it would be nice to know when the function had mapped the entire array). Then we shall have to define an internal function that will be the equivalent of our tick method above, and we shall have to make sure it knows how far we’ve got into the array.

Enter a closure. Of course. I’m sure you were expecting it. Here’s the code:

Array.prototype.mapAsync = function (process, done, context) {
var i = 0,
result = [],
last = this.length,
self = this,
processAsync = function () {
if (i in self) {
result[i] = process.call(context, self[i], i);
}
if (++i === last) {
done.call(context, result);
}
else {
processAsync.delay();
}
};
processAsync();
};

 

Let’s take this slowly. First of all we declare the same three locals as before. Then I save the value of the this variable, that is, the array this method is acting on. (I like using the name self for this purpose.) Then I’ve declared a function and it is this function that’s going to be called through our delay mechanism. If you notice at the end of the mapAsync method, I kick it all off by calling processAsync() for the first time.

I want to stop here and ask you to consider that call. It’s known as a function invocation. I’m not calling processAsync on an object (there’s none to be seen), in which case it would have been a method invocation call. Since there’s no object on which it’s called, JavaScript will call it using the global object. Inside the function the this variable will be bound to the global object. And that’s why, when you now look at the implementation of processAsync, you can see why we need to save the value of the this variable from the outer function – we would have no other way to get at the array.

So, let’s take a look at that inner function. First of all it processes the current element, determined by the captured value of i. Yes, we’ve created a closure with the mapAsync function and all its local variables have been captured and are available to the inner processAsync function. Now we increment the counter and if we’ve reached the end of the original array, we can call the done callback, setting up the right context (that is, the this variable for done), and passing the resulting, mapped array. If we haven’t reached the end of the array, we call processArray again, but delaying it slightly.

Here’s the code that’ll test it:

var myArray = [2, 3, 5, 7, 11, 13];
myArray[7] = 19; // missing out myArray[6]
console.log(myArray);

myArray.mapAsync(function (element, index) {
return "<" + index.toString() + ": " + element.toString() + ">";
}, function (a) {
console.log(a);
});

Notice that we now have two anonymous functions acting as callbacks: the first will process each element and the second will log the value of the mapped array passed in, once all the elements of the array have been processed. In Firebug, I get this:

[2, 3, 5, 7, 11, 13, undefined, 19]
undefined
["<0: 2>", "<1: 3>", "<2: 5>", "<3: 7>", "<4: 11>", "<5: 13>", undefined, "<7: 19>"]

Again notice that the result of the call to mapAsync (that is, “undefined”) is logged before the mapped array is logged.

If you think about it, this mapAsync code is showing the use of many callbacks: there’s process, done, and there’s processAsync, which is passed as a callback to the setTimeout function. Yep, callbacks in JavaScript are used everywhere. Get used to them and the use of anonymous functions to define them.

Next time we’ll work on refining the chunks of code we execute at one time. Processing every element through a delay is a little too granular.

Explore data-driven apps with less coding and query writing, brought to you in partnership with Qlik.

Topics:

Published at DZone with permission of Julian Bucknall, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}