Odložte VŠECHNY věci

James Socol je můj manažer v Mozille a je to epický šéf. Obvykle je váš manažer někdo, kdo se snaží porozumět podstatě kódu webové aplikace; pekelník, James Socol napsal kód. James byl úžasným pomocníkem při vývoji mého Pythonu, ale zde vstoupil do světa JavaScriptu a promluvil o Deferreds.

JavaScript byl jedním z prvních jazyků, které jsem se kdy naučil, ale jak jsem se více posouval směrem k vývoji Pythonu, dostával jsem se stále více mimo kontakt s nejlepšími způsoby, jak věci dělat.

Pak mě přítel požádal o pomoc s projektem. Server byl v Node.js a úložiště dat bylo MongoDB. Async, webscale, buzzwords, to všechno jsme měli!

Zpětné volání do pekla

Jednou z nevýhod toho, že je vše asynchronní, je, že je velmi snadné spadnout do pasti, kdy vše zapíšete jako anonymní zpětné volání. Obecně lze říci, že k tomu došlo, když se podíváte na konec souboru.

                            });
                        });
                    });
                // OH GOD WHY
                });
            });
        });
    // CLOSE EVERYTHING
    });
});

Tohle mě prostě bolí. Je to z konce pohledu, který získal předměty ze dvou kolekcí. Jako vývojář Pythonu mohu udělat totéž ve čtyřech řádcích kódu!

def home(request):
    foos = Foo.objects.all().orderby('-created')[0:5]
    bars = Bar.objects.all().orderby('-created')[0:5]
    return render(request, 'home.html', {'foos': foos, 'bars': bars})

Ne, není to asynchronní (no...), ale myslím, že bych raději strávil cykly CPU a šetřil si mozek.

Vlastně jsem zdržel návrat k JavaScriptu, protože hodně z toho vypadá takto.

Proč?

Proč to tak nenávidím?

Když jsou zpětná volání vnořena takto, obvykle se při přístupu k proměnným spoléhají na lexikální uzávěr, např.:

app.get('/', function(req, res) {
    // Open the connection.
    db.open(function(err, db) {
        // Get one collection.
        db.collection('users', function(err, usersColl) {
            // Search the first collection.
            usersColl.find({}, {'limit': 3}, function(err, usersCursor) {
                // Convert the result into an array.
                usersCursor.toArray(function(err, users) {
                    // Get the second collection.
                    db.collection('articles', function(err, artColl) {
                        // Search the second collection.
                        artColl.find({}, {'limit': 3}, function(err, artCursor) {
                            // Convert the result into an array.
                            artCursor.toArray(function(err, articles) {
                                // Now we have two arrays, users and articles, in scope.
                                // Render the home page.
                                res.render('home.ejs', {'users': users, 'articles': articles});

Nejvnitřnější funkce má přístup pouze k poli users protože je uzavřena kvůli několika dalším funkcím.

Jsou na tom nejméně 3 věci:

  1. Oba dotazy jsou asynchronní, ale provádíme je sériově, nikoli paralelně. To je ztráta času a spálí většinu našich „asynchronních“ výhod hned od začátku.
  2. Protože tyto vnitřní funkce závisí na lexikálním uzavření, nelze je testovat v menších fragmentech a je obtížné je refaktorovat, aby se staly testovatelnějšími.
  3. Vykreslení šablony uprostřed databázového dotazu není o nic lepší než databázový dotaz uprostřed šablony:
<h1><?= mysql_query($my, "SELECT title FROM posts WHERE..."); ?></h1>

A nakonec #4, Ó BOŽE HNÍZDĚNÍ.

Ale pak!

O měsíce později, v den hackerů, když jsem zkoumal něco úplně jiného, ​​jsem narazil na jQuery.Deferred() a bylo to, jako bych vyšel z jeskyně a viděl světlo.

Viděl jsem projekty jako tame.js, které se mi v zásadě líbí, ale v praxi mi přijdou příliš složité. Myslel jsem, že to jsou možnosti:peklo zpětného volání nebo předkompilátory, které chrlí hromady neproniknutelného kódu.

Najednou by moje funkce mohly být zase funkcemi! Mohli by přijmout argumenty, místo aby se spoléhali na rozsah. Mohly by být provozovány paralelně. Mohly by být pěkné a ploché!

var mongodb = require('mongodb');
var express = require('express');
var Deferred = require('Deferred');

var app = express.createServer();
var db = new mongodb.Db('mysite',
                        new mongodb.Server('localhost', 27027, {}),
                        {native_parser: true});

var Deferred = require('Deferred');

// Only do this once. Keep it somewhere else.
function getArray(db, coll, search, options) {
    var def = Deferred();
    db.open(function(err, db) {
        if (err) def.reject(err);
        db.collection(coll, function(err, collection) {
            if (err) def.reject(err);
            collection.find(search, options, function(err, cursor) {
                if (err) def.reject(err);
                cursor.toArray(function(err, arr) {
                    if (err) def.reject(err);
                    def.resolve(arr);
                });
            });
        });
    });
    return def.promise();
}

function home(req, res) {
    var uDef = getArray(db, 'users', {}, {'limit': 3});
    var aDef = getArray(db, 'articles', {}, {'limit': 4});
    var lookup = Deferred.when(uDef, aDef);
    lookup.done(function(users, articles)) {
        res.render('home.ejs', {'users': users, 'articles': articles});
    }).fail(function(err) {
        res.render('error.ejs', {'error': err})
    });
}

app.get('/', home);

Ach, to je lepší

Mnoho kódů knihoven, jako jsou Dojo a jQuery, využívá Promises a Deferreds, aby byly věci čistší. Doufám, že stále více a více uživatelských kódů udělá totéž.

Finding Deferreds mi pomohl naučit se znovu milovat JavaScript. Je to tak zábavný, flexibilní a zajímavý jazyk, takže jsem rád, že složitý kód lze snadno testovat a číst.