JSDeferred

Standalone and Compact asynchronous library in JavaScript.

Overview

JSDeferred is standalone and compact asynchronous library. Asynchronous codes are very confusing because it is a storm of callbacks. JSDeferred improve the readability of asynchronous codes by providing one object and some functions.

next(function () {
    alert('Hello,');
    return wait(3);
}).
next(function (r) {
    alert('World!');
});

"Standalone and Compact" means JSDeferred is very portable so works in many environment.

Download

Supported Environment

  • Browsers

    Offcourse, almost browsers are supported: Google Chrome, Opera, Microsoft Internet Explorer and etc.

  • node.js

    node.js provides many many asynchronous functions. JSDeferred helps program readability.

  • Chrome Extension

    Google Chrome provides HTML5 based extension feature. JSDeferred can be used with it.

  • Titanium

    Titanium Mobile SDK can create iPhone and Android application by JavaScript. JSDeferred can work in it.

  • Greasemonkey

    JSDeferred is standalone and compact. So you can just copy and paste your userscripts.

In fact, JSDeferred only depends on setTimeout() function, so if setTimeout is provided, JSDeferred will work.

Tutorial

Loading Scripts

To use JSDeferred, add a script element to the HTML

<script type="text/javascript" src="jsdeferred.js"></script>
<script type="text/javascript" src="my.js"></script>

JSDeferred is stand-alone, and does not depend on any external libraries, so that loading jsdeferred.js is sufficient in order to use it. The codes below are what would be written in my.js.

The First Step

Loading JSDeferred defines the Deferred object. For convenience, we export a set of functions to the global scope by Deferred.define(). You don't, of course, need to export those functions at all.

Deferred.define();

By doing this, you can use useful functions such as next(), loop(), call(), parallel() and wait() as global functions. Let's write some asynchronous process.

next(function () {
    alert("Hello!");
    return wait(5);
}).
next(function () {
    alert("World!");
});

This is a process that alerts Hello!, then alerts World! after 5 seconds of delay.

The above code is exactly the same as the following. It can be used even if you don't choose to export the functions using Deferred.define().

Deferred.next(function () {
    alert("Hello!");
    return Deferred.wait(5);
}).
next(function () {
    alert("World!");
});

Comparison with ordinary callback processes

What's the advantage of writing in such style.

If you give a function as a callback, then asynchronous processes are written as a nest of functions. For example, if you want to fetch /foo.json, /bar.json, /baz.json in this order.

// http.get is assumed to be a function that takes a URI and a callback function as arguments
http.get("/foo.json", function (dataOfFoo) {
    http.get("/bar.json", function (dataOfBar) {
        http.get("/baz.json", function (dataOfBaz) {
            alert([dataOfFoo, dataOfBar, dataOfBaz]);
        });
    });
});

You see here that the nesting of functions get deeper and deeper as you have more asynchronous processes. What if you want get an arbitrary number of data?

var wants = ["/foo.json", "/bar.json", "/baz.json"];
// How would you write that?

It's too cumbersome to do it. Let's do it with the Deferred.

// http.get is assumed to be a function that takes a URI as an argument and returns a Deferred instance
var results = [];
next(function () {
    return http.get("/foo.json").next(function (data) {
        results.push(data);
    });
}).
next(function () {
    return http.get("/baz.json").next(function (data) {
        results.push(data);
    });
}).
next(function () {
    return http.get("/baz.json").next(function (data) {
        results.push(data);
    });
}).
next(function () {
    alert(results);
});

The code is longer, but the processes are in serial. I can even combine the part occurring three times.

var wants = ["/foo.json", "/bar.json", "/baz.json"];
var results = [];
loop(wants.length, function (i) {
    return http.get(wants[i]).next(function (data) {
        results.push(data);
    });
}).
next(function () {
    alert(results);
});

Now it's shorter, and can handle any number of requests. "loop" is a function that, if a Deferred instance is returned in the argument function, it waits until the deferred process to finish and then execute the following process.

The above is a code to fire requests sequentially, i.e. load foo.json then bar.json and so on, you probably have more situations where you want to load them all at once. In that case, you can simply write as this.

parallel([
    http.get("/foo.json"),
    http.get("/bar.json"),
    http.get("/baz.json")
]).
next(function (results) {
    alert(results);
});

"parallel" is a function that executes the following process after all Deferred instances have finished their processes. It's that simple, isn't it?

Error Handling

What's useful about Deferred is its error handling. Browsers like Firefox kills errors occurred during an asynchronous process without raising the error console. How would you debug such a case?

I normally surround the asynchronous process with a "try {} catch (e) { alert(e) }", but it's tedious to do it every time.

JSDeferred can create an error-back flow apart from its normal callback flow. For example, have a code like this.

next(function () {
    // something 1
}).
next(function () {
    // asynchronous process
    throw "error!";
}).
next(function () {
    // something 2 (not executed as an error occurs in the previous process)
});

Now you want to handle exceptions.

next(function () {
    // something 1
}).
next(function () {
    // asynchronous process
    throw "error!";
}).
next(function () {
    // something 2 (not executed as an error occurs in the previous process)
}).
error(function (e) {
    alert(e);
});

You just need add .error(). It can catch all the exceptions that occur before the .error() part.

In the above code the "something 2" won't be executed because of the exception, but if you want to execute it no matter if you get an error, then write like this.

next(function () {
    // something 1
}).
next(function () {
    // asynchronous process
    throw "error!";
}).
error(function (e) {
    alert(e);
}).
next(function () {
    // something 2 (executed since the exception would already be handled)
}).
error(function (e) {
    alert(e);
});

You can slide an error handling in the middle. The process after the error() is always executed unless you get another exception in the error() process.

Chain

If a Deferred instance is returned in a function given to a Deferred process, it waits for the returned Deferred.

next(function () {
    alert("Hello!");
    return wait(5);
}).
next(function () {
    alert("World!");
});

In the code above, wait() is a function to return a Deferred that "waits for 5 seconds". In this case, the following process waits for the returned Deferred to execute. You can return any other function than wait, which returns a Deferred.

next() also returns a Deferred instance, so you can write as this.

next(function () {
    alert(1);
    return next(function () {
        alert(2);
    }).
    next(function () {
        alert(3);
    });
}).
next(function () {
    alert(4);
});

This is executed in the numerical order.

"Deferredize" a Function

When you use JSDeferred, you will often find it useful to define a custom function return a Deferred, rather than having it take a callback in a normal fashion. It's very easy to do it indeed. As an example of XMLHttpRequest, I'm going to define the http.get which we saw a few times above.

http = {}
http.get = function (uri) {
    var deferred = new Deferred();
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                deferred.call(xhr);
            } else {
                deferred.fail(xhr);
            }
        }
    };
    deferred.canceller = function () { xhr.abort() };
    return deferred;
}

Create a Deferred instance with "new Deferred()", and call its "call()" method within an asynchronous callback. It will execute processes associated to the callback chain of the Deferred.

Similarly, calling the "fail()" method fires an error. If you want to catch the exception using ".error()", you need to call the "fail()" appropreately.

I also defined the canceller. It's executed when the cancel() method of the Deferred instance is called. Normally you don't use it much, but you can remember that it exists.

Coding an Asynchronous Process

When you write a code that depends on Deferred, it is convenient if you write all asynchronous processes as functions to return a Deferred instance. Even if you don't need any process to follow it, it makes you easy to put it into a Deferred chain.

If you are writing a general library, it may be good to first write functions to take callback functions, and then create a function to Deferredize them.

Dividing a Heavy Process

Handling DOM a large number of times with JavaScript can be very heavy for some browsers. As a result, browser's UI process such as scrolling gets stuck. This can be very stressful for the users, so you should avoid such a thing to happen.

Of course, it is essential to write an efficient code, but the DOM handling could be inevitable. In that case, dividing a process and running asynchronously can make the browser UI smooth.

The loop() function of JSDeferred can give back control to the browser after each loop.

loop(1000, function (n) {
    // heavy process
});

Imagine writing this without JSDeferred…… I wouldn't dare to do it.

Automatically Divide a Long Loop

loop() function is effective when each loop is a heavy process. However, when a single loop is not so heavy but the number of iteration is numerous, it's not efficient. Here I define a function called repeat()

function repeat (n, f) {
    var i = 0, end = {}, ret = null;
    return Deferred.next(function () {
        var t = (new Date()).getTime();
        divide: {
            do {
                if (i >= n) break divide;
                ret = f(i++);
            } while ((new Date()).getTime() - t < 20);
            return Deferred.call(arguments.callee);
        }
    });
}

This is a loop that is automatically divided up every 20 msec. Unlike the ordinary loop(), it cannot wait even if a Deferred instance is returned in a loop.

Cookbook

Basic Chain

next(function () {
    console.log("start");
}).
next(function () {
    function pow (x, n) {
        function _pow (n, r) {
            console.log([n, r]);
            if (n == 0) return r;
            return call(_pow, n - 1, x * r);
        }
        return call(_pow, n, 1);
    }
    return call(pow, 2, 10);
}).
next(function (r) {
    console.log([r, "end"]);
}).
error(function (e) {
    alert(e);
})

Basic Loop

loop(10, function (i) {
    console.log(i)
});

Deferred Ajax

$.get("README.markdown").next(function (data) {
    console.log(data);
});

Parallel Deferred

Array of deferreds:

console.log("start. gathering data.");

parallel([$.get("README.markdown"), $.get("ChangeLog")]).
next(function (values) {
    var lengths = $.map(values, function (i) { return i.length });
    console.log(lengths.join(", "));
});

Named parallel deferreds:

console.log("start. gathering data.");

parallel({html: $.get("README.markdown"), js: $.get("ChangeLog")}).
next(function (values) {
    console.log(["html=", values.html.length, " js=", values.js.length].join(""));
});

Arbitrary number of deferreds in array:

console.log("start. wait 3 sec.");

var list = [];
var printAndReturn = function (i) { console.log(i+"msec elapsed"); return i; };
list.push(wait(0).next(printAndReturn));
list.push(wait(1).next(printAndReturn));
list.push(wait(2).next(printAndReturn));
list.push(wait(3).next(printAndReturn));

parallel(list).next(function (values) {
    console.log("Completed. values: "+values.join(", "));
});

Workers

var queue   = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var workers = new Array(2);
var work    = function (job) {
    console.log('working... ' + job);
    return wait(Math.random() * 4);
};

for (var i = 0, len = workers.length; i < len; i++) {
    workers[i] = next(function me () {
        var job = queue.shift();
        if (!job) return;

        console.log("start worker: " + job);
        return next(function () { return job }).
        next(work).
        next(me);
    }).
    error(function (e) {
        alert(e);
    });
}

parallel(workers).next(function () {
    console.log('all done!');
});

Divided Loop

next(function () {
    var sum = 0;
    return loop({end:100000, step:1000}, function (n, o) {
        console.log(["Processing divided loop:n=", n, ", sum=", sum, " last?=", o.last].join(""));
        for (var i = 0; i < o.step; i++) {
            // console.log(i + n);
            sum += i + n;
        }
        console.log(["sum=", sum].join(""));
        return sum;
    });
}).
next(function (e) {
    console.log("Result:"+e);
    console.log("end");
}).
error(function (e) {
    console.log(e);
});
loop({begin: 1, end:100, step:10}, function (n, o) {
    console.log(["Processing divided loop:n=", n, " last?=", o.last].join(""));
    for (var i = 0; i < o.step; i++) {
        var j = n + i;
        console.log(j);
    }
});

Auto Divided Loop

Deferred.repeat(100, function (n, o) {
    console.log(n);
    for (var i = 0; i < Math.pow(n, 2); i++) {
        for (var j = n; j; j--);
    }
});

Pseudo Multi Thread

loop(10, function (n) {
    console.log(n);
    return wait(0.1);
});

loop(10, function (n) {
    console.log(String.fromCharCode(97+n));
    return wait(0.2);
});

Shorthand

console.log(0);
loop(10, function (n) {
    console.log(n);
    return n;
}).
wait(1).
loop(10, function (n) {
    var c = String.fromCharCode(97+n);
    console.log(c);
    return c;
}).
next(function (i) {
    console.log("end");
}).
error(function (e) {
    alert(e);
});

Delay Loop

loop(5, function (i, o) {
    console.log(i);
    return o.last? i : wait(1);
}).
next(function (e) {
    console.log("end ["+e+"]");
}).
error(function (e) {
    console.log(e);
});
next(function (i) {
    function delayloop (i) {
        console.log(i++);
        if (i < 5) {
            return wait(1).next(function () {
                return call(delayloop, i);
            });
        }
    }
    return call(delayloop, 0);
}).
next(function (e) {
    console.log("end");
}).
error(function (e) {
    console.log(e);
});

Step Run (event handling)

var deferred = Deferred();
$("#step-run").click(function () { deferred.call() });

loop(5, function (i) {
    console.log("running... " + i);
    return deferred;
}).
next(function () {
    console.log("completed");
});

Brainfuck interpreter

function bfrun () {
    var mem    = $("<pre/>"), out = $("<pre/>"), button = $("#bfi-run").hide();
    var sinput = $("#bfi-source").hide(), source = sinput.val();
    var shtml  = $("<pre class='code'/>");
    var selems = $.map(source.split(/\n/), function (l) {
        var line = $("<div class='line1'/>").appendTo(shtml);
        return $.map(l.split(""), function (c) {
            return $("<span/>").append(c).appendTo(line);
        })
    });
    sinput.before(mem).before(out).before(shtml);

    var pi = {
        "+": function (c) {
            if (!c.stack[0]) return c;
            c.memory[c.pointer]++;
            return c;
        },
        "-": function (c) {
            if (!c.stack[0]) return c;
            c.memory[c.pointer]--;
            return c;
        },
        ">": function (c) {
            if (!c.stack[0]) return c;
            c.pointer++;
            c.memory[c.pointer] = (c.memory[c.pointer] || 0);
            return c;
        },
        "<": function (c) {
            if (!c.stack[0]) return c;
            c.pointer--;
            c.memory[c.pointer] = (c.memory[c.pointer] || 0);
            return c;
        },
        ".": function (c) {
            if (!c.stack[0]) return c;
            c.output.push(String.fromCharCode(c.memory[c.pointer]));
            return c;
        },
        ",": function (c) {
            if (!c.stack[0]) return c;
            c.memory[c.pointer] = "t";
            return c;
        },
        "[": function (c) {
            if (c.memory[c.pointer] == 0) {
                c.stack.unshift(false);
            } else {
                c.stack.unshift(c.pos - 1);
            }
            return c;
        },
        "]": function (c) {
            var s = c.stack.shift();
            if (s) c.pos = s;
            return c;
        }
    };

    next(function () {
        return {
            pos     : 0,
            pointer : 0,
            memory  : [0],
            stack   : [true],
            output  : [],
            source  : source
        };
    }).
    next(function (c) {
        if (selems[c.pos - 1]) selems[c.pos - 1].removeClass("em");
        var chr, fun;
        do {
            chr = c.source.charAt(c.pos);
            fun = pi[chr];
            c.pos++;
        } while (chr.match(/[^<>,.\[\]+-]/));
        var m = c.memory.concat();
        m.splice(c.pointer, 1, "*"+(c.memory[c.pointer] || 0));
        mem.text(m.join(", "));
        out.text(c.output.join(""));

        if (typeof fun == "function") {
            c = fun(c);
            if (c.pos < c.source.length) {
                if (selems[c.pos - 1]) selems[c.pos - 1].addClass("em");
                return call(arguments.callee, c);
            } else {
                return c;
            }
        } else {
            return c;
        }
    }).
    next(function (c) {
        var reset = $("<input type='button' value='Reset' class='button'/>")
        sinput.after(reset);
        reset.click(function () {
            sinput.show();
            button.show();
            mem.remove();
            out.remove();
            shtml.remove();
            reset.remove();
        });
        console.log("end");
    }).
    error(function (e) {
        alert(e);
    });
}

Slideshow

  • Load external resources
  • Preload next image
  • Infinite loop
var img = $('img#slideshow-image');

function loadImage (url) {
    console.log("loadImage: " + url);
    var d = new Deferred();
    var img = new Image();
    img.src = url;
    img.onload = function () {
        d.call();
    };
    return d;
}

function showImage (url) {
    var d = new Deferred();
    img.one('load', function () {
        d.call();
    });
    img.attr('src', url);
    return d;
}

console.log("Loading photo list...");
$.ajax({
    url : 'http://picasaweb.google.com/data/feed/base/user/cho101101?alt=json&kind=photo&hl=ja&access=public&callback=?',
    dataType: 'jsonp',
    data : {
        'fields'      : 'entry(title,link,content,media:group)',
        'start-index' : 1,
        'max-results' : 3,
        'thumbsize'   : '144c'
    }
}).
next(function (data) {
    console.log("Loading photo list... done");
    var n = 0;
    img.attr('src', data.feed.entry[0].media$group.media$thumbnail[0].url);
    return next(function () {
        n = (n + 1) % data.feed.entry.length;

        var entry = data.feed.entry[n];
        var url   = entry.media$group.media$thumbnail[0].url;
        return parallel([
                wait(3),
                loadImage(url) // preload
            ]).
            next(function () {
                return img.fadeTo(250, 0).promise();
            }).
            next(function () {
                return showImage(url);
            }).
            next(function () {
                return img.fadeTo(250, 1).promise();
            }).
            next(arguments.callee);
    });
}).
error(function (e) {
    alert(e);
});

callcc

like Scheme's callcc

function callcc (fun) {
    var error = new Deferred();
    return call(function () {
        // JSDeferred passes current Deferred Object to  this.
        var ccdeferred = this;
        // Call with current continuation (calling Deferred.next)
        return fun(function (a) { ccdeferred._next.call(a); throw error });
    }).
    error(function (e) {
        // Current Deferred chain must be stopped
        if (e === error) {
            return e;
        } else {
            throw e;
        }
    });
}

callcc(function (cont) {
    return 10 * 10 * cont(20);
}).
next(function (val) {
    console.log("callcc1 returns:" + val);
});
// should show "callcc1 returns:20"

var cont;
var i = 0;
callcc(function (c) {
    cont = c;
    return 10;
}).
next(function (val) {
    console.log("callcc2 returns:" + val);
    if (!i++) cont(20);
});
// should show "callcc2 returns:10", "callcc returns:20"

and Scheme's amb

function callcc (fun) {
    var error = new Deferred();
    return call(function () {
        // JSDeferred passes current Deferred Object to  this.
        var ccdeferred = this;
        // Call with current continuation (calling Deferred.next)
        return fun(function (a) { ccdeferred._next.call(a); throw error });
    }).
    error(function (e) {
        // Current Deferred chain must be stopped
        if (e === error) {
            return e;
        } else {
            throw e;
        }
    });
}

// http://www.sampou.org/scheme/t-y-scheme/t-y-scheme-Z-H-16.html#node_chap_14
// Just port above.
function amb () {
    var alts = arguments;
    var prevAmbFail = amb.ambFail;

    return callcc(function (sk) {
        return loop(alts.length, function (i) {
            var alt = alts[i];
            return callcc(function (fk) {
                amb.ambFail = function () {
                    amb.ambFail = prevAmbFail;
                    return fk("fail");
                };
                return sk(alt);
            });
        }).
        next(prevAmbFail);
    });
}
amb.ambFail = function () { throw "amb tree exhausted" };

// Utility function
function amb1 (ambvars) {
    var f    = wait(0);
    var vars = {};
    for (var k in ambvars) if (ambvars.hasOwnProperty(k)) (function (name, val) {
        console.log(name);
        f = f.next(function () {
            return amb.apply(this, val).next(function (i) {
                vars[name] = i;
                return vars;
            });
        });
    })(k, ambvars[k]);

    return f;
}

function assert (cond) {
    if (!cond) throw amb();
}

// http://mayokara.info/note/view/251
Array.prototype.uniq = function(){
    for (var i = 0,l = this.length; i < l; i++) {
        if (this.indexOf(this[i]) < i) {
            this.splice(i--, l-- && 1);
        }
    }
    return this;
};

amb1({
    baker    : [1, 2, 3, 4, 5],
    cooper   : [1, 2, 3, 4, 5],
    fletcher : [1, 2, 3, 4, 5],
    miller   : [1, 2, 3, 4, 5],
    smith    : [1, 2, 3, 4, 5]
}).
next(function (vars) { with (vars) {
    console.log(vars);
    // 簡易 distinct
    assert([baker, cooper, fletcher, miller, smith].uniq().length == 5);
    console.log("distinct passed");
    assert(baker  != 5);
    assert(cooper != 1);
    assert(fletcher != 1 && fletcher != 5);
    assert(miller > cooper);
    assert(Math.abs(smith - fletcher)  != 1);
    assert(Math.abs(fletcher - cooper) != 1);

    return vars;
} }).
next(function (vars) { with (vars) {
    console.log("solved");
    console.log(vars);
    alert(uneval(vars));
} }).
error(function (e) {
    alert(e)
});

Behavior

Deferred structure and chain structure

Deferred structure

A Deferred object has only one callback as its process. Deferred object packages a process (function) as callback and has reference to next Deferred (this is thought like continuation).

Example for understanding Deferred structure.

var d1 = new Deferred();
d1.callback.ok = function () {
    alert("1");
};

var d2 = new Deferred();
d2.callback.ok = function () {
    alert("2");
};

// Set d2 as continuation of d1.
d1._next = d2;

// Invoke the chain.
d1.call();

And example for usual use.

next(function () { // this `next` is global function
    alert("1");
}).
next(function () { // this `next` is Deferred#next
    alert("2");
}).
next(function () {
    alert("3");
});

Deferred#next creates new Deferred, sets the passed functions to process of it, sets it as continuation of `this` and returns it.

This structure makes easy to chain child Deferreds.

next(function () {
    alert("1");
}).
next(function () {
    alert("2");
    // child Deferred
    return next(function () {
        alert("3");
    });
}).
next(function () {
    alert("4");
});
Chain child deferred

When the callback returns Deferred, the Deferred calling the callback only sets its continuation (`_next`) to returned Deferred's continuation.

next(function () {
    alert("1");
}).
next(function () {
    alert("2");
    var d = next(function () {
        alert("3");
    });
    d._next = this._next;
    this.cancel();
}).
next(function () {
    alert("4");
});

After the process, above code is same as following:

next(function () {
    alert("1");
}).
next(function () {
    alert("2");
    next(function () {
        alert("3");
    }).
    next(function () {
        alert("4");
    });
});

Error processing and recovering

A Deferred has also error-back for error processing. Let's just see an example (this is from test):

next(function () {
    throw "Error";
}).
error(function (e) {
    expect("Errorback called", "Error", e);
    return e; // recovering error
}).
next(function (e) {
    expect("Callback called", "Error", e);
    throw "Error2";
}).
next(function (e) {
    // This process is not called because
    // the error is not recovered.
    ng("Must not be called!!");
}).
error(function (e) {
    expect("Errorback called", "Error2", e);
});

The error thrown in callback is propagated by error-back chain. If the error-back returns normal value, the error is considered as recovery, and the callback chain continues.

Different-origin Deferred instances

JSDeferred can be used in inter-environment which is independent respectively like browser extension because JSDeferred determines a self-class identity by instance id.

Development

Repository

JSDeferred is hosted on github.

$ git clone git://github.com/cho45/jsdeferred.git

Test

Browser Tests or CUI tests:

$ node test-node.js 
or
$ rhino test-rhino.js
or
$ phantomjs test-phantomjs.js

Active Issues

Closed Issues

Contributors

Fork me on GitHub