Skip to content

Instantly share code, notes, and snippets.

@iofjuupasli
Last active August 29, 2015 14:26
Show Gist options
  • Save iofjuupasli/a198f2e3f1ecce2918f9 to your computer and use it in GitHub Desktop.
Save iofjuupasli/a198f2e3f1ecce2918f9 to your computer and use it in GitHub Desktop.
async graphs in js

Comparison of several ways of working with complex async graphs in js

  A     E
 /|\    |
B | C   F
 \|/
  D

This is dependency graph. Every letter is async task. Every line is dependency - from bottom to top. So D can be evaluated only after A, B and C.

Task complete only after all subtasks evaluated.

It's important to start tasks in parallel and as soon as possible.

Most popular approach is callbacks (not for this task but overall):

function (done) {
    var subtasksComplete = 0;
    var _d;
    AEval(function (err, a) {
        if (err) {
            return done(err);
        }
        var dDeps = {};

        function dDepsFullfiled() {
            DEval(a, dDeps.b, dDeps.c, function (err, d) {
                if (err) {
                    return done(err);
                }
                _d = d;
                subtasksComplete++;
                if (subtasksComplete === 2) {
                    done(null, d);
                }
            });
        }
        BEval(a, function (err, b) {
            if (err) {
                return done(err);
            }
            dDeps.b = b;
            if (dDeps.c) {
                dDepsFullfiled();
            }
        });
        CEval(a, function (err, c) {
            if (err) {
                return done(err);
            }
            dDeps.c = c;
            if (dDeps.b) {
                dDepsFullfiled();
            }
        });
    });
    EEval(function (err, e) {
        if (err) {
            return done(err);
        }
        FEval(e, function (err, f) {
            if (err) {
                return done(err);
            }
            subtasksComplete++;
            if (subtasksComplete === 2) {
                done(null, _d);
            }
        });
    });
}

Oh my callback hell! So shitty. If you can do it better - don't hesitate, post me please. But anyway unlikely it will be much better.

Sure you know about async.js

function (done) {
    async.auto({
        a: function (done) {
            AEval(done);
        },
        b: ['a', function (done, results) {
            BEval(results.a, done);
        }],
        c: ['a', function (done, results) {
            CEval(results.a, done);
        }],
        d: ['a', 'b', 'c', function (done, results) {
            DEval(results.a, results.b, results.c, done);
        }],
        e: function (done) {
            EEval(done);
        },
        f: ['e', function (done, results) {
            FEval(results.e, done);
        }]
    }, function (err, results) {
        done(err, results.d);
    });
}

Way better. Now it's actually possible to read the code and understand developer's intent.

You still have some problems related to callbacks, e.g. calling it twice.

But let's try Promise.

function () {
    var dP = AEval()
        .then(function (a) {
            var bP = BEval(a);
            var cP = CEval(a);
            return [a, bP, cP];
        })
        .spread(DEval)
    var fP = EEval()
        .then(FEval);
    return Promise.join(dP, fP, function (d) {
        return d;
    });
}

It isn't standart Promise but bluebird. async.js also isn't standart library, so in comparison there is no difference.

It's most common way to work with promises. I've seen this in some libs and projects.

It describes threads of async tasks nicely, so 'after A you can evaluate b and c' and so on.

But there is another one approach for Promises. Let's compare

function () {
    var aP = aEval();
    var bP = aP.then(bEval);
    var cP = aP.then(cEval);
    var dP = Promise.join(aP, bP, cP, DEval);
    var eP = EEval();
    var fP = eP.then(FEval);
    return Promise.join(dP, fP, function (d, f) {
        return d;
    });
}

Oh wow! So plain. Looks almost like syncronious code.

Sync... oh I heard about 'async looks like sync'.

co - your turn:

co(function* () {
    var a = yield aEval();
    var b = yield bEval(a);
    var c = yield cEval(a);
    var d = yield dEval(a, b, c);
    var e = yield eEval();
    var f = yield fEval(e);
    return d;
});

Just compare with first callback-based version! There is no better solution in the world, isn't it?

Ok, I hope I didn't catch you.

This so nice looking code doesn't runs in parallel (eEval runs only after dEval). It means that first version with callbacks in fact is better.

Here is the fixed version:

co(function* () {
    var dP = co(function* () {
        var a = yield aEval();
        var bP = bEval(a);
        var cP = cEval(a);
        var b = yield bP;
        var c = yield cP;
        return yield dEval(a, b, c);
    });
    var fP = co(function* () {
        var e = yield eEval();
        return yield fEval(e);
    });
    yield fP;
    return yield dP;
});

Not sure whether it possible to make it better. You know, help me if you can.

Hmm, but it doesn't look so nice as previous.

Some of you have questions:

"This stars* and yields - what does it mean?"

"Can I use it in browser?"

You have some troubles here.

In fact it's magic. I doubt that generators was created for this.

Summary

callback - hell

async - ok, but verbose, and it still callbacks

promise chains - nice for simple chains, but tricky for complex graphs

promise variables - best for complex graphs. Ok for simple

co - for simple async chain looks really nice. But also tricky for complex. And you have troubles with browsers

The Winner

I think Promises is the best way to describe complex async graphs.

Promises were created for that.

You can use it in node or browser without building step.

For simple chains you can use chains of then. In comparsion with co it will looks almost the same.

For complex async graphs it's better to create variable for each promise.

@evenfrost
Copy link

Well, some believe Promises are just 'intermediate' level on the road to ES7 async/await. And I think it's not about syntax sugar but about improvements in management of JS control flow. As you correctly mentioned, we can write all the stuff in old palmy ES3, though in this case (if no 3rd party libs are used) we'll fall into callback hell in any scenario. That's why promises were introduced in ES6, and that's why there is async/await spec in ES7. :)
And in my opinion _await_s look a bit cleaner than _then_ables, but everybody ought to do his own thing for sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment