ugrás a tartalomhoz

Aszinkron JavaScript programozás

MadBence · 2013. Jún. 25. (K), 16.26
Aszinkron JavaScript programozás

A cikkben a JavaScript egyik misztikus jószágát, az aszinkron programozási lehetőségeket járom körbe, és próbálok a felmerült problémákra minél több, minél egyszerűbb programozási megoldást keresni.

Az olvasó részéről a cikk feltételezi, hogy tisztában van a JavaScript nyelv szintaktikai sajátosságaival, magabiztosan mozog a környezetben. A bemutatott módszerek példakódjainál nem a robusztusságot helyeztem előtérbe, inkább proof of concept jellegű megoldásoknak szánom őket, ebből következően többnyire nem teljesek, nem optimálisak, pusztán egy-egy minta vázlataként szolgálnak.

Event loop

A JavaScript alapvetően egyszálú környezetben fut, az alkalmazásunk szempontjából azonban fontos, hogy reszponzív maradjon. Mit is jelent ez?

A JavaScript esemény alapú modellben gondolkozik. A koncepció nem új, gyakorlatilag az összes grafikus alkalmazás ugyanezen az elven működik. Csak hogy egy példát említsek: ha írtunk már programot nyersen a WinAPI segítségével, akkor is pontosan ugyanezt a modellt használtuk:

MSG message;

while (GetMessage(&message, 0, 0, 0)) {
    TranslateMessage(&message);
    DispatchMessage(&message);
}

Ez volt az alkalmazás lelke. A program ugyanúgy egyszálú volt, az események (MSG struktúra) pedig egy végtelen ciklusban (ez az event loop, eseménykezelő hurok) kerültek feldolgozásra. A trükköt a GetMessage() végzi, egy olyan rendszerhívás, ami az alkalmazás eseménysorából kiveszi a soron következő eseményt, illetve ha nincs, akkor vár (a CPU pörgetése, terhelése nélkül). Az eseménysor egy egyszerű FIFO adatszerkezet, semmi bonyolult.

Ha az eseménykezelő valamilyen hosszadalmas dolgot csinált (pl. while (1);), akkor a program lefagyott, hiszen nem tudta feldolgozni a többi eseményt (kattintás a bezárás ikonra stb.), a végtelen ciklus miatt sosem jutott vissza a hurok elejére.

A JavaScript teljesen hasonlóan működik. Maga a ciklusmag el van rejtve előlünk (és nem is érdekel minket, hogy hogyan van megvalósítva), viszont megfelelően „okos” eseménykezelővel szintén le tudjuk fagyasztani a böngészőt vagy az aktuális böngészőfület (ez a böngésző architektúrájától függ leginkább, manapság az a tendencia, hogy minden fül saját folyamatban fut, de mindenképpen saját szálon):

document.body.onclick = function () {  
    while (1);
}

Egészen addig, amíg nem kattintunk, semmi sem történik, utána viszont leblokkol a UI (user interface, felhasználói felület) szála, melyen a JavaScript eseménykezelőnk is fut. A függvényünk soha nem tér vissza, így nem engedi, hogy bármi más futhasson.

Synchronous JavaScript And XML

Vegyünk egy kevésbé irreális példát, az (ilyen néven nem igazán használatos) SJAX-ot (Synchronous JavaScript And XML), ami rendelkezzen egy egyszerű API-val. Egy gombra kattintás után szeretnénk, ha letöltene egy erőforrást az internetről, és az eredményt elhelyezné az oldalon:

button.onclick = function () {
    /** 
     * A /hello címen található webszolgáltatás 
     * a 'Hello {name}!' sztringet adja vissza 
     */
    var result = SJAX.get('/hello?name=' + name);
    resultBox.innerHTML = result;  
}

Sajnos az SJAX hívásunk blokkolni fog, hiszen amíg nem épült föl a TCP kapcsolat, nem kerültek elküldésre a HTTP fejlécek, nem dolgozta fel a kérést a távoli szolgáltatás, és nem küldte vissza az eredményt, addig nem tudunk más eseményt feldolgozni. Mivel gyakorlatilag minden interakció esemény, az alkalmazás fagyott ezen időtartam alatt. Ha a csillagok megfelelően állnak, akkor persze ez nagyon rövid időintervallum (néhányszor 10 ms), de ha épp nem optimális a hálózat, akkor ez akár másodpercekig is eltarthat. Ez a felhasználói élmény szempontjából roppant előnytelen.

Asynchronous JavaScript And XML

Az igény tehát adott: szeretnénk, ha az ilyen hívásaink aszinkron jellegűek lennének. Fohászaink meghallgattattak, jelenleg széleskörűen elérhető a böngészőkben az XMLHttpRequest objektum, amit pontosan erre találtak ki (ez képes a szinkron kommunikációra is, de az imént tárgyalt hátránya miatt nem javallott a használata). Vegyünk ismét egy egyszerű példát, most AJAX-ra, egyszerűsített API-val:

button.onclick = function () {
    AJAX.get('/hello?name=' + name, function (result) {  
        resultBox.innerHTML = result;  
    });
}

Mi is történt itt? Az AJAX.get() azonnal visszatér, azaz az események feldolgozása tovább folyhat. Viszont regisztrálunk egy eseménykezelőt, ami akkor fog lefutni, amikor megkaptuk a hívás eredményét. Hasonlítsuk össze a két megoldást! A különbséget akkor értjük meg igazán, ha a szekvenciadiagramjukat megnézzük (jelölésben próbáltam az UML szabványhoz ragaszkodni, az objektumok/metódusok neve teljesen ad hoc jellegű, leginkább a könnyebb megértést szolgálja):

A szinkron működés miatt az eseménykezelő ciklusnak esélye sincs a futásra, tehát blokkolni fog.

Ez viszont trükkös! Az AJAX.get() hívás aszinkron, azaz a futás jogát visszaadja a hívó félnek (csak néhány olyan járulékos dolgot tesz, ami szükséges, ilyen például az eseménykezelő callback függvényünk regisztrálása). Az eseménykezelő ciklusunk tehát futhat, a felület nincs blokkolva.

Amikor a HTTP kérés befejeződött, az eseménysorba bekerül a hozzá tartozó esemény, amit az eseménykezelő ciklusunk nemsokára ki is vesz onnan, kikeresi a hozzá tartozó eseménykezelőt (a callback függvényünk), és meghívja azt.

The Pyramid of Doom

A koncepció remekül működik, megoldottuk a blokkolás problémáját. Helyette kaptunk egy másikat, ez a pyramid of doom. A probléma szemléltetésére nézzünk egy egyszerű NodeJS kódrészletet, amivel egy MongoDB adatbázison hajtunk végre egy lekérdezést:

connection.open(function (error, db) {
    if (error) {
        console.log(error);
        return;
    }
    
    db.collection('user', function (error, collection) {
        if (error) {
            console.log(error);
            return;
        }
        
        collection.find({age: {$gt: 18}}, function (error, cursor) {
            if (error) {
                console.log(error);
                return;
            }
            
            cursor.toArray(function (error, array) {
                if (error) {
                    console.log(error);
                    return;
                }
                
                array.forEach(function (user) {  
                    console.log(user.name);  
                });  
            });  
        });  
    });
});

A konkrét kód megértése nélkül is látszik, hogy valami nincs rendben. A probléma a nevét a fenti kódrészlet alakja miatt kapta. Még ha hozzá is szokik a szemünk az ilyen jellegű kódszervezéshez, semmiképpen sem egyszerű a megértése. Mi van akkor, ha az aszinkron függvény után futtatunk kódot, milyen sorrendben dolgozunk? A válasz természetesen itt most triviális, de ez nem jelenti azt, hogy minden esetben ilyen egyszerű kódunk van. Akár néhány elágazást beszúrva olyan komplexitású kódot kapunk, amit lehetetlen karbantartani.

Még számtalan probléma van ezzel a kóddal, de most koncentráljunk csak magára a piramis alakra (a többivel majd később foglalkozunk).

Lapítás

Első próbálkozásként megpróbáljuk kilapítani a piramisunkat:

connection.open(connectionOpened);

function connectionOpened(error, db) {
    if (error) {
        console.log(error);
        return;
    }
    
    db.collection('user', collectionRetrieved);
}

function collectionRetrieved(error, collection) {
    if (error) {
        console.log(error);
        return;
    }
    
    collection.find({age: {$gt: 18}}, cursorRetrieved);  
}

function cursorRetrieved(error, cursor) {
    if (error) {
        console.log(error);
        return;
    }
    
    cursor.toArray(resultsFetched);
}

function resultsFetched(error, array) {
    if (error) {
        console.log(error);
        return;
    }
    
    array.forEach(printUser);
}

function printUser(user) {
    if (error) {
        console.log(error);
        return;
    }
    
    console.log(user.name);
}

A problémát nem oldottunk meg, csak elfedtük, ráadásul a sorrendiségnek is annyi (akár teljesen véletlenszerű sorrendben is felírhattuk volna a függvényeinket). A kód a goto spagetti szindróma jeleit mutatja. Debugoláskor ráadásul még a stacktrace sem nyújt semmilyen hasznos információt, hogy hogyan jutottunk el az adott függvénybe: emlékezzünk, itt az összes függvény (rendben, a forEach() kivétel) azonnal visszatér az eseményhurokba!

Hibakezelés

A másik probléma a hibák kezelése. A hibákat (amióta létezik kivételkezelés) throw-val (vagy ekvivalens nyelvi elemmel) dobjuk, majd valahol később azt egy catch blokkban kapjuk el. A callback alapú modellben ez nem működik, hiszen hiába tesszük például a connection.open() hívást egy try-catch blokkba, nem fogunk tudni elkapni hibákat, hiszen amikor az eredményt megkapjuk, a függvényünk már régen visszatért az eseményhurokba.

Támadhat persze az az ötletünk, hogy akkor adjunk át hibakezelő callback függvényeket is, de ezzel csak magunk alatt vágjuk a fát, hiszen bonyolítjuk az amúgy sem egyszerű kódunkat.

Vezérlési szerkezetek?!

A következő probléma, hogy viszonylag bonyolult ebben a modellben párhuzamosan, illetve sorosan (valamint ezek kombinációjában) műveleteket végezni. Természetesen felhasználhatjuk a már meglévő könyvtárakat (pl. Async) a problémáink orvoslására, de ezek leginkább csak tüneti kezelést nyújtanak.

Csak hogy valami fogalmunk legyen, írjuk meg a párhuzamos, illetve a soros végrehajtásra képes utility függvényeket. Párhuzamos végrehajtás alatt itt természetesen nem valódi párhuzamos végrehajtást kell érteni (egy szálon futunk!), hanem azt, hogy egymás után több aszinkron feladatot is elindítunk.

/** 
 * A megadott `job`-okat sorosan lefuttató függvény.
 * 
 * Egy job egy függvény, aminek első paramétere egy callback függvény, amit
 * akkor kell meghívni, ha végzett az aszinkron művelet.
 * A függvénynek tetszőleges számú paraméter átadható, a következő job
 * ezeket megkapja.
 * 
 * @param jobs A feladatok tömbje
 */  
function waterfall(jobs) {
    var i = 0;
    
    function next() {
        i < jobs.length && jobs[i++].apply(null, [next].concat(Array.prototype.slice.call(arguments)));
    }
    
    next();
}

Példa az egyszerű használatra:

// 3 mp alatt elszámol 3-ig  
waterfall([
    function (next) {
        console.log(1);
        setTimeout(next, 1000);
    }, function (next) {
        console.log(2);
        setTimeout(next, 1000);
    }, function (next) {
        console.log(3);
        setTimeout(next, 1000);
    }
]);

Nézzük meg ugyanezt párhuzamosan:

/**
 * A megadott `job`-okat párhuzamosan lefuttató függvény
 * 
 * A job egy függvény, aminek egy paramétere van, egy callback függvény,
 * amit a job befejezésekor kell meghívni, maximum 1 paraméterrel.
 * Ennek a paraméternek az értéke a job eredménye, a szinkronizációs
 * pontban lefutó függvény ezt az értéket kapja meg.
 * 
 * @param jobs A feladatok tömbje
 * @param finish A szinkronizációs pontban (amikor minden job befejeződött)
 *               lefutó függvény.
 */
function parallel(jobs, finish) {
    var results = [];
    var remaining = jobs.length;
    
    for (var i in jobs) {
        jobs[i]((function (i) {
            return function (result) {
                results[i] = result;
                
                if (!--remaining) {
                    finish(results);
                }
            }
        })(i));
    }
}

Szintén egyszerű példakód a működésre:

// 1, 2, 3 azonnal, majd 3 mp után done!
parallel([
    function (done) {
        console.log(1);
        setTimeout(done, 1000);
    }, function (done) {
        console.log(2);
        setTimeout(done, 2000);
    }, function (done) {
        console.log(3);
        setTimeout(done, 3000);
    }
], function (results) {
    // a results tömbben lenne a jobok eredménye, sorrendhelyesen
    console.log('done!');
});

Természetesen ezek csak egyszerű Móricka-implementációk, nem foglalkozunk a hibakezeléssel, esetleges timeout-okkal.

Promise

Használjuk a már meglévő eszközeinket az aszinkronitás problémáinak orvoslására! Az egyik lehetséges út a promise-ok használata. A promise egy olyan művelet eredményét reprezentálja, aminek az értéke a jelenben nem ismert. Az API azonban biztosít néhány eseményt (tipikusan success és error), amikre fel lehet iratkozni. Létezik egy ajánlás az interfészre, a Promises/A, ami tekinthető de facto, és létezik tucatnyi programkönyvtár (Q, node-promise, jQuery.Deferred stb.), ami többé-kevésbé implementálja ezt az interfészt.

Implementáljunk egy delay() függvényt, ami várakoztatja a futást, és a fentebb leírt tulajdonságokkal rendelkezik:

function delay(ms) {
    var successHandler = null;
    
    setTimeout(function () {
        successHandler && successHandler();
    }, ms);
    
    var promise = {
        then: function (f) {
            successHandler = f;
        }
    };
    
    return promise;
}

Ezek után a meghívása valahogy így történhet:

delay(1000).then(function () {
    console.log(1);
});

Ez eddig nem tűnik túl érdekesnek, sőt, a hagyományos callback megoldásokhoz képest sem nyújt újat. A kód nem lett olvashatóbb, és a pyramid of doom sem tűnt el:

delay(1000).then(function () {
    console.log(1);
    
    delay(1000).then(function () {
        console.log(2);
        // és így tovább...
    });
});

A másik probléma, hogy minden aszinkron kódunkhoz (itt a delay()) hozzákötöttük a promise implementációnkat, válasszuk tehát külön a dolgokat.

Vezessük be a deferred objektum fogalmát. Ez egy aszinkron művelet reprezentációja, alapvetően két dolgot tud: sikeresen (resolve()), illetve sikertelenül végződni (reject()). Nem szeretnénk, ha ezt a két függvényt rajtunk kívül bárki meg tudná hívni, így a műveletet el kell választanunk az eredményétől (magára a műveletre nem kíváncsi a hívó fél, csak az eredményre). A Deferred-hez tartozik egy Promise objektum, a külvilág ezt kapja meg, ezen keresztül tud feliratkozni az eseményekre. Lássuk hát a kezdetleges implementációnkat:

var Promise = (function () {
    function Promise() {}
    
    Promise.prototype.then = function (success, error) {
        this._success = success;
        this._error   = error;
    }
    
    return Promise;
}());

var Deferred = (function () {
    function Deferred() {
        this.promise = new Promise();
    }
    
    Deferred.prototype.resolve = function () {
        this.promise._success && this.promise._success.apply(null, arguments);
    };
    
    Deferred.prototype.reject = function () {
        this.promise._error && this.promise._error.apply(null, arguments);
    }
    
    return Deferred;
}());

Írjuk át a delay() függvényünket, hogy az új API-t használja:

function delay(ms) {
    var defer = new Deferred();
    
    // A `bind`-re csak azért van szükség, hogy a `this`
    // helyes értéket kapjon a `resolve` függvényben
    setTimeout(defer.resolve.bind(defer), ms);
    
    return defer.promise;
}

Készen van a minimálimplementációnk, viszont a piramisunk ugyanúgy megvan. A promise minta attól lesz igazán hatékony eszköz a kezünkben, hogy láncolható. Azaz a then() függvény szintén egy Promise objektumot ad vissza, ami akkor lövi el a success eseményét, ha a then() függvényben átadott függvény eredménye is ismert, azaz ha az is egy Promise-t ad vissza, akkor annak a feloldásakor. Lássuk tehát az okos(abb) Promise implementációnkat:

var Promise = (function () {
    function Promise() {
        // jobb híján elhisszük, hogy akinek az isPromise attribútuma igazra
        // értékelődik ki, az egy Promise...
        this.isPromise = true;
    }
    
    Promise.prototype.then = function (success, error) {
        var defer = new Deferred();
        
        this._success = function () {
            var r = success.apply(null, arguments);
            
            if (r && r.isPromise) {
                r.then(defer.resolve.bind(defer), defer.reject.bind(defer));
            }
        }
        
        this._error = function () {
            var r = error.apply(null, arguments);
            
            if (r && r.isPromise) {
                r.then(defer.resolve.bind(defer), defer.reject.bind(defer));
            }
        }
        
        return defer.promise;
    }
    
    return Promise;
}());

A műveleteink így már láncolhatóak:

delay(1000).then(function () {
    console.log(1);
    return delay(1000);
}).then(function() {
    console.log(2);
    return delay(1000);
});

Eltűnt a piramis, a végrehajtás szekvenciális. Még egy dolog maradt hátra, a hibakezelés. Szeretnénk, ha a throw-val eldobott hibákat el tudnánk kapni, és a try-catch blokkokhoz hasonlóan a hibát nem feltétlenül a keletkezés helyén szeretnénk kezelni. Ehhez manuálisan kell megvalósítani a kivételek eljuttatását a megfelelő helyre. Nézzük meg tehát a végső Promise implementációnkat:

var Promise = (function () {
    function Promise() {
        this.isPromise = true;
    }
    
    Promise.prototype.then = function (success, error) {
        var defer = new Deferred();
        
        function makeFunction(f) {
            return function () {
                try 
                    var r = f.apply(null, arguments);
                    
                    if(r && r.isPromise) {
                        r.then(defer.resolve.bind(defer), defer.reject.bind(defer));
                    } else {
                        defer.resolve(r);
                    }
                } catch (e) {
                    defer.reject(e);
                }
            }
        }
        
        this._success = typeof success === 'function' ? makeFunction(success) : defer.resolve.bind(defer);
        this._error   = typeof error   === 'function' ? makeFunction(error)   : defer.reject.bind(defer);
        
        return defer.promise;
    }
    
    return Promise;
}());

A kipróbáláshoz vegyük elő ismét az adatbázisos példánkat. A példában egy szimulált adatbázishoz fogunk csatlakozni (setTimeout()-ok alkalmazása, szimulált hibák), és az eredeti API a hagyományos callback alapú lesz, (error, result) szignatúrájú callback függvényekkel (utolsó paraméterként).

var connection = function () {
    var connection = {
        open: function (f) {
            setTimeout(function () {
                if (Math.random() > 0.9) {
                    f('Hiba az adatbázishoz csatlakozás közben!');
                } else {
                    f(null, db);
                }
            }, 1000);
        }
    };
    
    var db = {
        collection: function (name, f) {
            setTimeout(function () {
                if (Math.random() > 0.8) {
                    f('Hiba a `' + name + '` kollekció lekérése közben!');
                } else {
                    f(null, coll);
                }
            }, 1000);
        }
    };
    
    var collection = {
        find: function (condition, f) {
            setTimeout(function () {
                if (Math.random() > 0.7) {
                    f('Hiba a lekérdezés közben a `' + JSON.stringify(condition) + '` feltétel mellett!');
                } else {
                    f(null, cursor);
                }
            }, 1000);
        }
    };
    
    var cursor = {
        toArray: function (f) {
            setTimeout(function () {
                if (Math.random() > 0.6) {
                    f('Hiba az eredmény tömbbé konvertálása közben!');
                } else {
                    f(null, array);
                }
            }, 1000);
        }
    };
    
    var array = [{name: 'Sándor'}, {name: 'József'}, {name: 'Benedek'}];
    
    return connection;
}();

Írjuk meg a függvényt, ami tetszőleges callback alapú API-ból képes Promise-t gyártani!

function promisify (f) {
    return function () {
        var defer = new Deferred();
        
        var args = Array.prototype.slice.call(arguments, 0);
        
        f.apply(null, args.concat([function (error, resolution) {
            if (error) {
                defer.reject(error);
            } else {
                defer.resolve(resolution);
            }
        }]));
        
        return defer.promise;
    }
}

Végül pedig használjuk az elkészült API-t a promisify() függvénnyel kombinálva. Természetesen szerencsésebb lenne, ha az API köré egy wrapper objektumot írnánk, de a célnak ez is megfelel.

console.log('Csatlakozás...');

promisify(connection.open)().then(function (db) {
    console.log('A `user` kollekció lekérése...');
    return promisify(db.collection)('user');
}).then(function (collection) {
    if(Math.random() > 0.7) {
        throw new Error('Húha, én mégsem ezt akartam!');
    }
    
    console.log('Keresés...');
    return promisify(collection.find)({age: {$gt: 18}});
}).then(function(cursor) {
    console.log('Találatok tömbbé konvertálása...');
    return promisify(cursor.toArray)();
}).then(function (array) {
    array.forEach(function (user) {
        console.log('Név: ' + user.name);
    });
}, function (error) {
    console.log('Hiba: ' + error);
});

Vegyük sorra a régi problémákat, és nézzük meg, találtunk-e rá megoldást:

  • Az átláthatatlan, egymásba ágyazott callback függvényeket megszüntettük.
  • A sorrendiség adott, ha akarunk sem tudunk suttyomban rossz sorrendben kódot végrehajtani.
  • A hibakezelés egységes és egyszerű, throw használható.

Coroutines

Az aszinkronitás problémájára megoldást nyújt a (koránt sem újkeletű) coroutine-ok használata, illetve valójában kistestvéreik, a generátorok az a koncepció, ami jelenleg bontogatja a szárnyait JavaScript környezetben.

Mit jelent ez számunkra? Vegyük a szinkron hello world webszolgáltatásos példánkat újra elő:

button.onclick = function () {
    // az `await` kulcsszót C#-ból köcsönöztem
    var result = await betterAJAX.get('/hello?name=' + name);
    resultBox.innerHTML = result;
}

Coroutine-ok használatával a fenti kódrészlet futása így zajlik le (sajnos szekvenciadiagramon ez nem túl szemléletes):

  1. Meghívódik az onclick függvényünk
  2. Meghívódik a betterAJAX függvényünk
  3. Az onclick visszatér
  4. Az eseménykezelő ciklus reagál az eseményekre
  5. Ha megjött a HTTP válasz, a hozzá tartozó esemény bekerül az eseménysorba
  6. Az onclick függvényünk folytatja a futását (a result változóba bekerül az eredmény)
  7. Az eredmény megjelenik a megfelelő DOM elemben, majd visszatér a függvény

De hogyan is lehetséges ez a fajta működés? Közelítsük meg a problémát az értelmező (interpreter) oldaláról. A JavaScript, hasonlóan sok más programozási nyelvhez, egy darab stack-kel rendelkezik. Ez viszont ilyen esetekben nem lesz elég: hisz az onclick-ben rendelkezünk egy stack-kel, majd (miközben várunk az eredményre) az eseménykezelő hurok más függvényhívásokat is indít, amik saját stack-kel rendelkeznek. Az eredmény megérkezése után helyre kell állítanunk a futás állapotát, ezt a stack segítségével tudjuk megtenni, ez tehát nem veszhet el. Látszik, hogy ezeknek a problémáknak a megoldása nem egyszerű, az értelmezőt fel kell készíteni (stack elmentése, betöltése, illetve az eredmény szempontjából lényegtelen, hogy hogyan oldja meg), illetve ennek megfelelően az ECMAScript szabványt is bővíteni kell új kulcsszavakkal (az ECMAScript jelenleg kidolgozás alatt álló 6-os verziója tartalmazza a specifikációt).

A másik lehetőség, ha egy plusz réteget iktatunk be az interpreter és a saját kódunk közé: egy fordítót (compiler), ami át tudja alakítani a kódunkat, hogy az a JavaScript jelenlegi verziója számára is emészthető maradjon, nekünk viszont biztosítsa a kényelmet. Természetesen ha egy komplett fordítót vonunk be a képbe, akkor egyáltalán nem kötelező a JavaScript szintaktikájánál maradnunk. A fordítókra talán a legjobb példa a CoffeeScript, illetve az IcedCoffeeScript, ami a korutinokhoz hasonló funkcionalitást támogat, de számtalan egyéb megoldás is létezik (Kaffeine, tamejs stb.). Ha a hagyományos JS szintaktikával szimpatizálunk, akkor is vannak eszközök (traceur, Mascara stb.), ami ES6 lehetőségeket használó JS kódot fordít ES5 szabványos kódra. A fordító az átalakítást (nagy vonalakban) úgy végzi, hogy felbontja a függvényünket az ilyen „szakadások” mentén, és egy nagy switch-be ágyazza, így amíg mi egyetlen függvényt látunk, aminek gyakorlatilag tetszőleges pontjára be tudunk lépni, az valójában sok kicsi, és a switch segítségével tudunk a megfelelő helyre ugrani (és persze a belső állapot tárolása is meg van oldva).

A korutinok/generátorok jelenleg nem túl elterjedtek, mivel a böngészők közül csak a Firefox implementálja (és ez az implementáció sem szabványos). Szerveroldalon a fibers nyújt ilyen jellegű funkcionalitást.

Generátorok

Barátkozzunk össze a yield kulcsszóval. Ha függvénybe tesszük (márpedig oda tesszük, hiszen máshol elhelyezve hibát kapunk), akkor az a függvény generátor (más néven iterátor, igaziból ez szemantika kérdése) lesz. Mi is az a generátor? Vegyük például a négyzetszámokat. Írjunk egy generátort, amivel elő tudjuk állítani az összes négyzetszámot:

function squares() {
    var i = 1;
    
    while (true) {
        yield i * i;
        i++;
    }
}

Vegyük használatba:

var s = squares();
for (var i = 0; i < 10; i++) {
    console.log(s.next());
}

A next() függvény hatására a generátorunk elkezd futni, majd a yield kulcsszónál található értékkel visszatér (most fogjuk föl úgy, mint egy return), viszont a következő next() hívásnál ugyaninnen folytatja a futását. Nekünk pont ez a funkcionalitás kell, használjuk hát ki.

Alkalmazás

Implementáljunk egy egyszerű AJAX-os hívást korutinok segítségével. Valójában ez generátor/iterátor, de a két fogalom szemantikai szempontból nem igazán jó elnevezés, szóval maradok a korutinoknál (hiszen a generátorok a korutinok részhalmaza). A használathoz a kódokat Firefoxban kell futtatni, egy type="application/javascript;version=1.7" attribútummal ellátott script blokkban:

job(function () {
    console.log('waiting...');
    var result = yield fakeAjax('/hello?name=weblabor');
    console.log(result);
});

Egy ilyen jellegű kódrészletet szeretnénk működésre bírni (a fakeAjax() trükkös jószág, egyszerűen egy setTimeout()-os késleltetéssel adja vissza az eredményt). Mi is történik itt? A yield fakeAjax() visszatér a függvényből a fakeAjax() visszatérési értékével. Ez a visszatérési érték egy függvény, ami egy paramétert vár, egy callback függvényt, amit akkor hív meg, amikor végzett az aszinkron művelettel.

function fakeAjax(url) {
    return function (done) {
        setTimeout(function () {
            done('Hello ' + url.substr(url.indexOf('?name=') + 6) + '!');
        }, 2000);
    }
}

Már csak egy olyan mechanizmust kell megírnunk, ami visszaadja a vezérlést az aszinkron művelet végén a függvényünknek, ez lesz a (varázslást végző) job() függvény.

function job(f) {
    var job = f();
    
    (function next(result) {
        try {
            var waitfor = job.send(result);
            
            waitfor(function (result) {
                next(result);
            });
        } catch (e) {
            // Ha a `job` végén nincs return, egy StopIterationExceptiont kapunk...
            if (!(e instanceof StopIteration)) throw e;
        }
    }());
}

A job() egy generátort (egy olyan függvényt, amiben van yield) vár, első lépésként példányosítja a generátort. Ez szintaktikailag megegyezik a függvény végrehajtásával, viszont fontos megjegyezni, hogy ekkor a függvény törzse nem értékelődik ki.

Ezek után a generátor send() függvényével elkezdi a futtatást – paramétere az előző (ha van) yield eredménye – és elkéri a következő értékét a generátornak. A kapott (függvényt) végrehajtja, átadva neki azt a callback függvényt, ami a következő aszinkron műveletet végrehajtja. A try-catch blokk azért kell, hogy ne kapjunk hibát az utolsó aszinkron művelet után (mivel utána nincs több yield, tehát a generátornak nincs következő értéke).

Coroutine + Promise = Task?

Végezetül próbáljuk meg a két módszert kombinálni! A promise minta rendkívül jó absztrakciót nyújtott az aszinkron műveleteink különbözőségeire, a korutinok pedig a szintaktikai bonyolultságot fedték el hatékonyan.

Módosítsuk a job() függvényünket, hogy az a Promise API-t használja:

function job(f) {
    var job = f();
    
    (function next(result) {
        try {
            var waitfor = job.send(result);
            
            (function usePromise(waitfor) {
                waitfor.then(function (result) {
                    next(result);
                }, function (error) {
                    usePromise(job.throw(error));
                });
            })(waitfor);
        } catch (e) {
            if (!(e instanceof StopIteration)) {
                console.log('Hoppá, el nem kapott kivétel!');
                console.log(e);
            }
        }
    }());
}

Érdekesség, hogy a Promise sikertelensége (error esemény) esetén hibát tudunk dobni, amit a hagyományos (try-catch) szintaktikával tudunk elkapni/feldolgozni.

Végül a teszt:

job(function () {
    try {
        console.log('Csatlakozás...');
        var db = yield promisify(connection.open)();
        
        console.log('A `user` gyűjtemény lekérése...');
        var collection = yield promisify(db.collection)('user');
        
        console.log('Keresés...');
        var cursor = yield promisify(collection.find)({age: {$gt: 18}});
        
        console.log('Találatok tömbbé konvertálása...');
        var array = yield promisify(cursor.toArray)();
        
        array.forEach(function (user) {
            console.log('Név: ' + user.name);
        });
    } catch (e) {
        console.log('Ajjajj, nagy a baj: ' + e);
    }
});

Az igazán szép persze az lenne, ha a job() is egy Promise-t adna vissza, ami természetesen nem elkapott kivétel esetén reject-et kap, a job végén pedig resolved-ot.

A Promise és korutinok kombinációját használja a task.js könyvtár, ami fenti naiv megoldásoknál sokkal robusztusabb, és több programozási lehetőséget biztosít.

Konklúzió

JavaScriptben számos lehetőségünk van, ha aszinkron módon szeretnénk programozni, ehhez rengeteg eszköz áll rendelkezésünkre, de egyik sem silver bullet, mindnek megvan a maga helye. Munkánk során mindig mérjük fel a lehetőségeinket, és ennek megfelelően válasszuk ki a használni kívánt megoldást. Viszont fontos, hogy ne csak használjuk, hanem értsük is a mögöttük húzódó koncepciókat, hiszen csak így tudunk objektívan dönteni, a feladathoz az optimális megoldást megtalálni.

Maga az aszinkron programozás rendkívül pezsgő terület, a közeljövőben várhatóan egyre több megoldás fog születni ezen a téren, én személy szerint bízok benne, hogy a böngészőkben (de természetesen szerver oldalon is) hamarosan el fog terjedni a korutinok használatának lehetősége.

A bélyegképen Paola fényképe látható.

 
1

Aszinkronitás

Hidvégi Gábor · 2013. Jún. 25. (K), 16.58
Gratulálok, szép munka!

Korábban már volt valamelyik blogmarkban vita erről, és ott is felvetettem ezt a gondolatot: hordoz-e információt az adott kontextusban az, hogy egy kérést aszinkron indítok el? Azon kívül, hogy elmondhatom: na, ez aszinkron történik, nem igazán, hisz csak az számít, hogy mit kapok vissza eredményül. Szóval szerintem az aszinkronitást nyugodtan elrejthetnék előlünk a futtatókörnyezetek.

Érdekesség: nemrég kiderült, hogy a legújabb Firefox egy fül bezárásakor az onunload eseményben kiküldött AJAX-hívást egyszerűen nem futtatja le (fél éve még nem csinált ebből problémát). Némi keresgélés után kiderült, hogy ilyenkor kivételesen szinkron módon kell kérni.
2

Némi keresgélés után

Joó Ádám · 2013. Jún. 25. (K), 17.22
Némi keresgélés után kiderült, hogy ilyenkor kivételesen szinkron módon kell kérni.


Ilyenkor késik a fül bezárása is?
3

Nem tudom

Hidvégi Gábor · 2013. Jún. 25. (K), 17.42
A válasz 60-70ms alatt szokott megérkezni, azaz legfeljebb ennyit. Mindenesetre majd kipróbálom, mi történik, ha beteszek egy sleep(1)-et.
6

hordoz-e információt az adott

MadBence · 2013. Jún. 25. (K), 19.43
hordoz-e információt az adott kontextusban az, hogy egy kérést aszinkron indítok el? Azon kívül, hogy elmondhatom: na, ez aszinkron történik, nem igazán, hisz csak az számít, hogy mit kapok vissza eredményül. Szóval szerintem az aszinkronitást nyugodtan elrejthetnék előlünk a futtatókörnyezetek.

Az elrejtés nem feltétlenül jó, sőt. Többek között ezért utálja mindenki a többszálú programozást:

if(i==0) {
  var j = foo();
  console.log(i);
}
A foo aszinkron, csak a futtatókörnyezet "elrejti" ezt. Annyit tudunk róla, hogy az i értékéhez hozzá sem nyúl, nem is hallott róla.
A kód havonta egyszer 4-et ír ki, ami miatt minden alkalommal háromcsillió pengő bevételkiesése van a cégnek. Sok sikert a debugoláshoz :)
7

Például a php-nak minden

Hidvégi Gábor · 2013. Jún. 25. (K), 20.50
Például a php-nak minden függvénye szinkron, és mégis működik a dolog, milliók használják boldogan. Biztosan vannak az általad idézetthez hasonló hibák, de nem jellemző.
8

van "aszinkron php" :)

MadBence · 2013. Jún. 25. (K), 21.10
van "aszinkron php" :) (natívan PHP-ban meg van írva az event loop, a hasonlóság nem véletlen), de nem teljesen értem, hogy jön ez ide.

Igen, a PHP-ban minden szinkron, azaz ha egy socketen nem jön adat, te pedig freadet (ami pl unix rendszereken valószínűleg egy read rendszerhívás lesz) küldesz rá, akkor a php futtatókörnyezete nem fog (nem tud) más php kódot futtatni. Nodejs esetében ez a rendszerhívás nem történik meg (azonnal), helyette az eseményhurok futtat egy select-et (a pontos megvalósítást nem ismerem, és nem is különösebben érdekel), tehát csak akkor történik olvasás, ha már van mit.
10

nem teljesen értem, hogy jön

Hidvégi Gábor · 2013. Jún. 25. (K), 21.26
nem teljesen értem, hogy jön ez ide
A következőt állítottad:
Az elrejtés nem feltétlenül jó, sőt.
Én ezt úgy értelmeztem, hogy szerinted jobb lenne mindent aszinkron meghívni (mivel ott nincs rejtés). Ezért írtam azt, hogy
Például a php-nak minden függvénye szinkron, és mégis működik a dolog
Azaz php-ban rejtés van mindenhol.
11

Úgy értettem, hogy az

MadBence · 2013. Jún. 25. (K), 21.32
Úgy értettem, hogy az aszinkron hívásokat nem szabad elrejteni (szinkronnak álcázni), mert codeflow szempontjából nagyon nem hasonlítanak a szinkron hívásokhoz. Funkcionalitás tekintetében persze azonosak, hiszen egy XHR kérés eredménye bármelyik úton módon meg fog érkezni.
12

Nem értem

Hidvégi Gábor · 2013. Jún. 25. (K), 21.58
codeflow szempontjából nagyon nem hasonlítanak a szinkron hívásokhoz
Miért nem hasonlítanak? Nem mindegy, hogy az általad példaként felhozott adatbázisból kérsz le valamit, vagy pedig a neten keresztül? Egyik esetben sem látom, hogy a várakozással töltött idő milyen információtöbbletet tartalmaz, csak az számít, hogy megérkezzen az adat.

Teljesen másak a felhasználói felületen bekövetkezett események, például egy billentyűleütés vagy egérkattintás, ott már lényeges az időfaktor is.
13

Miért nem hasonlítanak?A cikk

MadBence · 2013. Jún. 25. (K), 22.09
Miért nem hasonlítanak?
A cikk két szekvenciadiagramján látszik a különbség :)
14

Ha arra célzol, hogy AJAX

Hidvégi Gábor · 2013. Jún. 26. (Sze), 07.28
Ha arra célzol, hogy AJAX esetében a kérés válaszának megérkezéséig más eseményeket is fel tud dolgozni a rendszer, akkor azt értem, és nagyon szép is. Lépjünk eggyel feljebb: van egy gomb, aminek az a feladata, hogy ha rákattintanak, egy forrásból kér adatokat, amit végül fel kell dolgoznia. A feladat szempontjából milyen többletinformációt hordoz, hogy a kérést szinkron vagy aszinkron módon küldte? Ha félreértek valamit, javíts ki, kérlek.
15

Pont azért tehető meg a

MadBence · 2013. Jún. 26. (Sze), 08.14
Pont azért tehető meg a szinkron->aszinkron átalakítás, mert a feladat szempontjából lényegtelen, hogyan érkezik meg az adat. Hiába nincs köze a feladathoz, mégsem lehet/szabad elrejteni ezt, a fentebb említett debugolási veszélyek miatt. Így igenis hordoz információt a művelet típusa.
16

Nem értem

Hidvégi Gábor · 2013. Jún. 26. (Sze), 08.38
A fentebb említett debugolási veszélyek az összes php függvénynél is előfordulhatnak, a fejlesztői mégis ezt az utat választották, azaz az ilyen jellegű hibák javíthatóak; gondolom, ha előfordul ilyen hiba, akkor support keretében javítják magát a futtatókörnyezetet.

Ne haragudj, nem kekeckedni akarok veled, de tényleg meg szeretném érteni, hogy egy darab projekt havonta egyszer előforduló hibája miatt miért kéne egy olyan programozási módszert/szintaktikát alkalmazni, amiről az első percben kiderül, hogy nehézkes (callback hell), és különböző új szintaktikai elemeket és módszereket kell bevezetni, hogy hatékonyan lehessen dolgozni vele. Ennyi erővel egyébként minden függvényt aszinkron módon kéne meghívni a debugolási veszélyek miatt, nem?
20

A különbség egyszerű

szaky · 2013. Jún. 26. (Sze), 16.23
Igaziból nem is kell mást csinálni, mint kétszer egymásután gyorsan rákattintani a gombra, és máris előjönnek a különbségek, méghozzá elég sunyi módon.
26

Nem erre gondolok.Most

Hidvégi Gábor · 2013. Jún. 26. (Sze), 20.11
Nem erre gondolok.
Most hogyan működik egy AJAX kérés?
gomb.onclick = function gomb_klikk() {
  var xmlhttp = new XMLHttpRequest();
  xmlhttp.onreadystatechange=function() {
    if (xmlhttp.readyState==4 && xmlhttp.status==200) {
      document.getElementById('div').innerHTML = xmlhttp.responseText;
    }
  };
  xmlhttp.open('POST', 'ajax_info.txt', true);
  xmlhttp.send();
}

Mint látható, a kód teljesen fragmentált. Egyik helyen indítom, másik helyen fejezem be, ráadásul az esetleges korábban definiált változók - megoldástól függően - nem feltétlenül érhetők el. Az AJAX kérés kódját tehettem volna külön függvénybe, de a lényeg, hogy a visszakapott adatokat mindenképp másik függvényben tudom feldolgozni.

Mi lenne szerintem ideális? Tegyük fel, létezik egy Request nevű beépített objektum a javascriptben.
var Ajax_Keres = new Request();
Ajax_Keres.async = true;

gomb.onclick = function gomb_klikk() {
  var keres = Ajax_Keres.request({url: 'ajax_info.txt'});
  if (!keres.error) {
    document.getElementById('div').innerHTML = keres.responseText;
  }
}

A kontextus a gomb onclick eseménye, a feladat, hogy a kérésre érkezett választ rakjam be egy divbe. Ehhez ebben az esetben nem kell kilépnem a kontextusból, és máshonnan folytatni a feldolgozást, az esetlegesen definiált helyi változók is megmaradnak. Ez átláthatóbb és karbantarthatóbb kódot eredményez, és nincs szükség új szintaktika bevezetésére. A futtatókörnyezet foglalkozik az aszinkron kérés problémáival, előlem elrejti ezt, mert ebben a kontextusban nem érdekel, miként oldja meg. A kérés alatt természetesen kattintgathatok, gépelhetek, ami csak jólesik.
30

Ez azért nem oldható meg,

MadBence · 2013. Jún. 28. (P), 18.53
Ez azért nem oldható meg, mert amíg a vezérlés a request() függvényben van, addig az ott van, nem futhat más kód. Ha mégis futna, akkor az mindenféle nemdeterminisztikus viselkedést eredményezne, kvázi ezzel feltaláltuk a többszálú programozást. Ennek persze rengeteg járulékos dolga van, lockok, stb. Tehát semmiképpen sem egyszerű. Persze adott a másik út, amit a cikkben is említettem, hogy írni kell javascript fölé egy alkalmas fordítót, ami a kívánt szintaktikával rendelkező nyelvet natív javascriptre fordít.
31

Megoldható

Hidvégi Gábor · 2013. Jún. 28. (P), 19.18
SJAX kérés esetén is adhatsz meg az XMLHttpRequest objektumnak onreadystatechange eseményvezérlőt, és abban folyamatosan értesülhetsz a változásokról, azaz az általad említett debugolás is ugyanúgy megoldható, mint eddig. Innentől kezdve logikailag pontosan ugyanott vagy, mint AJAX kérés esetén, csak nem esik szét a kód.

Valószínűleg nagyjából úgy működik, hogy az eseménysor (queue) vezérlését megkapja és zárolja az XMLHttpRequest objektum, de attól függetlenül ő persze adhat hozzá új elemeket. Innentől kezdve egy paraméter kéne a kiegészítéshez, hogy az AJAX kérést is ugyanezzel a szintaktikával futtassa. És ha itt működik, átvehetné a dolgot mondjuk a node.js és a többi hasonszőrű nyelv, és (majdnem) az egész aszinkron programozásról el is felejtkezhetnénk. Jó a cikked, nagyon szépen összeszedett, de olyan problémára keres megoldást, aminek – kis túlzással – nem kéne léteznie. A böngészők onclick eseményéhez hasonlóak persze megmaradnának, de programlogikailag általában ez a legkevesebb.
57

Az ajax "soros" kéréseknél

inf · 2013. Júl. 2. (K), 12.20
Az ajax "soros" kéréseknél működhetne hasonló módon, mint az sjax. A párhuzamos kéréseket viszont kifelejtetted. Azt hogyan oldanád meg?
58

Milyen párhuzamos kérésekre

Hidvégi Gábor · 2013. Júl. 2. (K), 12.22
Milyen párhuzamos kérésekre gondolsz pontosan?
59

Mondjuk az említett gombra

inf · 2013. Júl. 2. (K), 12.28
Mondjuk az említett gombra kattintva még 2 másik box tartalmát frissíti a rendszer különböző adatforrásokból. Ugyanez megoldható szerver oldalon is, aztán el lehet küldeni egy csomagban az egészet, vagy megoldható kliens oldalon is aszinkron módon, és akkor jóval tisztább lesz a szerver oldal api-ja...

A nodejs-ben azért van szükség aszinkron kérésekre, mert a program, amit írsz nem egyetlen felhasználó egyetlen kérését szolgálja ki, így muszáj párhuzamosítani ahhoz, hogy egy kérés ne blokkolja le az összes többit. Körülbelül olyan, mint egy php daemon, amit több felhasználó is hívhat egyszerre. Gondolom ott is problémákat okoz a párhuzamos kérések feldolgozása...
60

Ha egy szerverről kell

Hidvégi Gábor · 2013. Júl. 2. (K), 12.33
Ha egy szerverről kell lekérdezni, mindenképp egy folyamattal szolgálnám ki az n darab dobozt, jóval gyorsabb a HTTP overhead miatt. Ahogy te írod, lehet, hogy a szerver api-ja tisztább lesz, de a kliensoldali kód meg piszkosabb.
61

Az a helyzet, hogy pont hogy

inf · 2013. Júl. 2. (K), 12.47
Az a helyzet, hogy pont hogy úgy lesz piszkosabb, ahogy te írod, mert be kell tenni még egy parsolót is, ami szétszedi három részre a választ, és szétosztja a boxok között... Amit én mondok ott valamivel gyorsabb lesz a kérés, viszont hátránya, hogy több erőforrást fog használni a több http kapcsolat miatt. Egyáltalán nem igaz, hogy minden esetben jobb szinkron módon kódolni, ahogy az sem igaz, hogy minden esetben jobb aszinkron módon kódolni. Feladata válogatja... A nodejs-ben azért minden aszinkron - ahogy már írtam én is, és mások is - mert ott több párhuzamos kérést dolgozol fel ugyanazzal a kóddal (mivel a szervert is te írod), ezzel szemben a php-nél csak egy kérést dolgoz fel egyszerre a futó kódod, így ott csak nagyon ritkán lenne jobb aszinkron módon írni a kódot. Itt tényleg csak arról van szó, hogy a párhuzamosítást melyik részébe toljuk a programnak. A php-nél az apache-ba tolták, és ő csinálja, a nodejs-nél meg a felhasználóra hárították, viszont adtak egy eszközt a kezébe, hogy ne kelljen thread-eket használnia minderre.

A gombos példánál maradva végülis hasonlóan piszkos:

    button.onclick = function () {  
        var result = SJAX.get('/hello?name=' + name);  
        box1.innerHTML = result.box1;
        box2.innerHTML = result.box2;
        box3.innerHTML = result.box3;
    }  
	
    button.onclick = function () {  
        AJAX.get('/hello1?name=' + name, function (result) {    
            box1.innerHTML = result;    
        }); 
        AJAX.get('/hello2?name=' + name, function (result) {    
            box2.innerHTML = result;    
        }); 
        AJAX.get('/hello3?name=' + name, function (result) {    
            box3.innerHTML = result;    
        }); 
    }  
Végülis nagy különbség nincs a két megoldás között sem a kliens, sem a szerver oldal terén. Egyszerűen csak más megközelítést használnak ugyanannak a problémának a megoldására.
62

Egyáltalán nem igaz, hogy

Hidvégi Gábor · 2013. Júl. 2. (K), 13.05
Egyáltalán nem igaz, hogy minden esetben jobb szinkron módon kódolni, ahogy az sem igaz, hogy minden esetben jobb aszinkron módon kódolni. Feladata válogatja...
Ez így van. Én egyik munkámban ezt használtam, a másikban azt, de nem vettem észre semmilyen hátrányát az SJAX-nak.

És nem azt akarom mondani, hogy az SJAX az annyira jó és az AJAX (és az aszinkron programozás) rossz. Az utóbbinál a szintaktika miatt a kód szétesik, kevésbé lesz átlátható, és közben nem is nyerünk vele semmit. Hát ezért lenne jobb, ha a futtatókörnyezetek ezt elrejtenék előlünk, és tudnánk olyat csinálni, hogy var result = AJAX.get('/hello?name=' + name);.

A példáddal kapcsolatban: ránézésre melyik mód az átláthatóbb és könnyebben karbantartható?
64

Azt hiszem ez elég

inf · 2013. Júl. 2. (K), 13.37
Azt hiszem ez elég nyilvánvaló :-)

Ez a probléma szerintem inkább abból fakad, hogy a js eleve szinkron megoldásokra lett kitalálva, nincsenek olyan nyelvi elemek benne, amik az aszinkron kódolást könnyebbé tennék. Most már van a yield 1.7-es verziótól, és ha megnézed a cikket, akkor a yield-es példa néz ki a legjobban... Esetleg ha még betesznek több ilyen nyelvi elemet, ami aszinkron kéréseknél segít, akkor kódolás szempontjából nem lesz nagy különbség a két változat között... Nem feltétlenül kell ezeknek ugyanúgy kinéznie, mint a szinkron kéréseknek, mivel nem azok, de lehetnek hasonlóan egyszerűek... A többletinformáció, amit hordoznak, hogy az utánuk jövő sorban lévő kód időben nem feltétlenül utánuk fog lefutni. Ezt nyelvi elemekkel javítani lehet. Szóval végülis igazad van abban, hogy jó lenne, ha fejlesztenék a nyelvet ilyen téren, abban viszont szerintem nincs, hogy el kellene rejteni azt, hogy egy kérés aszinkron, vagy szinkron.
65

A többletinformáció, amit

Hidvégi Gábor · 2013. Júl. 2. (K), 14.14
A többletinformáció, amit hordoznak, hogy az utánuk jövő sorban lévő kód időben nem feltétlenül utánuk fog lefutni.
Bocs, de nekem nagyon nehéz a felfogásom. Van a következő feladat, három lépésből áll:
1, adott egy gomb,
2, ha rákattintanak, akkor indítson kérést,
3, majd, ha megjött a válasz, frissítse a három doboz tartalmát.

gomb.onclick = function() {
  var result = AJAX.get('valami.php');
  if (!result) {
    return 'Nincs adat';
  }
  box1.innerHTML = result.box1;
  box2.innerHTML = result.box2;
  box3.innerHTML = result.box3;
};

Számodra, az oldal fejlesztője számára milyen többletinformációt jelent, hogy tudod: a kérés egy AJAX kérés?

Mi a különbség a fenti példa között, és aközött, hogy a 2-es pontban nem kérést indít, hanem egy, már a memóriában lévő tömb tartalmát kérdezed le egy függvénnyel, ami ugyanolyan formában adja vissza a választ, amit a 3-as pontban lévő függvénnyel dolgozol fel, és frissíted a három doboz tartalmát?

gomb.onclick = function() {
  var result = lokális_tömb_csócsál(adatok_tömbje);
  if (!result) {
    return 'Nincs adat';
  }
  box1.innerHTML = result.box1;
  box2.innerHTML = result.box2;
  box3.innerHTML = result.box3;
};

Én úgy látom, hogy nincs különbség az adott kontextusban (az onclick függvényben). Teljesen érdektelen számomra, honnan és miként kapom az adatokat. Egy dolog fontos, hogy ha nem érkezik adat meghatározott időn belül, akkor hibaüzenetet kell megjelenítenem, de ez a kontextus szempontjából érdektelen (mindkét esetben ki kell lépnem belőle).
69

A példád nem jó, mert csak

inf · 2013. Júl. 3. (Sze), 10.35
A példád nem jó, mert csak egyetlen kérésről van szó benne. Több kérés mehet sorban vagy párhuzamosan, vagy ezek tetszőleges kombinációjában. Ez a többletinformáció, amit az aszinkron kérések hordoznak. Szinkron kérések csak sorban mehetnek...
72

Ok, így már értem. A magam

Hidvégi Gábor · 2013. Júl. 3. (Sze), 10.58
Ok, így már értem. A magam részéről igyekszem egy kéréssel megoldani mindent.
17

Tudnál példát írni arra, hogy

Hidvégi Gábor · 2013. Jún. 26. (Sze), 08.45
Tudnál példát írni arra, hogy a fenti programrészlet hogyan nézne ki aszinkron hívással és debugolással?
4

Egy stack és a Firefox

presidento · 2013. Jún. 25. (K), 18.57
A JavaScript, hasonlóan sok más programozási nyelvhez, egy darab stack-kel rendelkezik.

Ebből következne az, hogy amíg szinkron lekérésre várok, más nem történhet, Firefox esetén azonban belefutottam egy hibába, miszerint blokkolt futás (szinkron lekérés, modális popup, stb.) közben kiváltódhat más esemény, szerintük:
"Synchronous" XHR is not actually a blocking synchronous call: it spins the event loop before it returns (you can tell because the page doesn't stop painting, videos keep playing, the page can be scrolled, etc; not spinning the event loop would break the web). So the only question is which events delivery is suppressed for....

Szerencsére ez csak Firefoxnál van így.

Nagyon jó a cikk, külön örülök, hogy a generátorokra is kitértél.
5

Nem tudom, hogy a FF belseje

MadBence · 2013. Jún. 25. (K), 19.26
Nem tudom, hogy a FF belseje hogyan működik, de ilyen mágiák előfordulhatnak (bár ez szerintem is bug) :). A szinkron vs aszinkron hívásokra talán egy "böngészőbiztosabb" példa:
function syncSleep(ms) {
  var s=new Date().getTime();
  while(new Date().getTime()-s < ms);
}
function asyncSleep(ms, cb) {
  setTimeout(cb, ms); // :)
}

console.log('async.start');
asyncSleep(1000, function({ //1mp után jönnie kéne a végének, de nem jön
  console.log('async.end');
});
console.log('sync.start');
syncSleep(5000); //mert ez bizony blokkol
console.log('sync.end');
Firefoxban is "jó" :)
18

Amit javasolsz, azt lehet

Thor · 2013. Jún. 26. (Sze), 15.11
Amit javasolsz, azt lehet bug-nak nevezni a for ciklussal miatt, még viccnek is rossz.
19

Nem teljesen értelek, a

MadBence · 2013. Jún. 26. (Sze), 16.21
Nem teljesen értelek, a firefoxnak kutya kötelessége lenne kifagyni a szinkron műveletek idejére, ezt meg is teszi az iménti példakódomban (tehát helyesen működik, csak nem azt adja, amit a laikus várna), viszont a linkelt bug szerint a szinkron XHR hívás mégsem szinkron.
22

Egyrészt AJAX hívásról volt

Thor · 2013. Jún. 26. (Sze), 17.29
Egyrészt AJAX hívásról volt szó, ahol az ilyen while (de akár for) ciklusos lockolás vagy timeout amatőr megoldás (de ajánlott mindehol kerülni), mert egy tucat más dologra is figyelni kell és ezért nulla az értéke a példakódodnak.

Másrészt a példádban szereplő
"syncSleep(5000); //mert ez bizony blokkol"
félrevezető, mert csak részben igaz, ugyanis VALÓBAN blokkolja a javascript motort, de NEM blokkolja a böngésző eseménykezelőit, azaz ha a egy gombra klikkelés indítja a hívást, akkor jön az igazi torkosborz, mert 20 kattintás 20*5 másodpercre fagyasztja a böngészőt (mindent). Gratulálok a "megoldásodhoz".

Egyébként a Firefox bughoz én annyit fűznék hozzá, hogy alapvetően igaza van mindenkinek.. tényleg bug-nak tűnik, de a fickó se irt hülyeséget. A probléma inkább ott van, hogy miért használ valaki (2011-ben) sync-es hívást? Vagy ha használ miért bízza a böngészőre a "lockolást"? Ezekre csak rossz válasz adható.

Egyébként, ha már..
.. tetszik a cikked, egy problémám van vele, hogy a végeredmény, amit kihoztál belőle a végén is szörnyű. A legnagyobb probléma vele, hogy teljesen átláthatatlan, fejleszthetetlen az egész. Tökéletes arra, ha egy cégnél olyan kódot akarsz írni, amit rajtad kívül senki nem "ért", csak pár óra "forráskód" böngészés után és kvázi "kirúghatatlan" leszel tőle.
Tipikus javascriptes agymenés, amivel tele van a net, holott az adott problémákra évtizedek óta van tisztább, szárazabb érzésű megoldás, ami javascriptben is működik.
23

Ácsi

Hidvégi Gábor · 2013. Jún. 26. (Sze), 17.44
Azért kicsit kulturáltabban is megfogalmazhattad volna a véleményed, látszik, hogy rengeteg munka van a cikkben, Bence ennyi tiszteletet megérdemel. Nem muszáj vele egyetérteni, ha tudsz jobbat, az info kukacra küldd be nyugodtan a saját irományod, de akár megoszthatod a kommentek között is.
27

Elnézést kérek, igazad van.

Thor · 2013. Jún. 26. (Sze), 20.19
Elnézést kérek, igazad van.
24

Természetesen javascripttel

MadBence · 2013. Jún. 26. (Sze), 18.30
Természetesen javascripttel nem lehet blokkolni az operációs rendszert, az események fel fognak torlódni (szerintem sehol sem állítottam az ellenkezőjét), ezért hívják event queue-nak az event queue-t. Preemptív ütemezés mellett elég nyilvánvaló, hogy semmi sem fog blokkolni.

Ha példakódok ha nem világosak, szívesen elmagyarázom őket :), a cikk elején direkt ezzel kezdtem: "többnyire nem teljesek, nem optimálisak,", azaz a lényeg az, hogy működik egy felvázolt részprobléma megoldására, senkinek sem ajánlom, hogy álljon neki saját implementációkat hegeszteni...

Én nyitott vagyok az újdonságokra, szóval szívesen hallanék más megoldásokról is.
25

A hozzászólásod első

Thor · 2013. Jún. 26. (Sze), 19.57
A hozzászólásod első bekezdésében a lényeg, hogy a példád semmire nem jó,
max. egy Hello world-ben használható, de a fenti AJAX-os probléma megoldására semmiképp.

Ami a cikket illeti és a más megoldást illeti.
Az adott (konkrét) problémára ami a "The Pyramid of Doom"-nál kezdődik,
egy objektum alapú megközelítés és megoldás az "igazi", az amit leírtál semmiképp
( az okokat már az előző hozzászólásomban leírtam), mint ahogy "Promises" megoldása (lentebb) is sokkal átláthatóbb vele.. még ha az túl általános is.
A Tied is nyilván működhet, csak "kicsit" bonyolultabban és "kicsit" rossz működő megoldást mutatva, mivel ennél egyszerűbben is lehet javascriptben jobban és szebben programozni.
29

Most tulajdonképpen melyik

MadBence · 2013. Jún. 28. (P), 18.41
Most tulajdonképpen melyik megoldás nem szép szerinted? :-)
33

Egy ilyesmi megoldás (a

Thor · 2013. Jún. 28. (P), 19.39
Egy ilyesmi megoldás (a piramisra ):
var Conn = function();
Conn.prototype =
{
    open: function()
    {
         this.state = "OPEN";
         database.open( this.process  );
    },
    process: function( error, object )
    {
         if ( error )
            console.log( error );

         switch ( this.state )
         {
            "OPEN":
                    this.STATE = "COLLECTION";
                    object.collection('user', this.process );
                    break;
            "COLLECTION":
                    this.STATE = "FIND";
                    object.find({age: {$gt: 18}}, this.process );
                    break;
            "FIND":
                    this.STATE = "TO_ARRAY";
                    object.toArray( this.process );
            "TO_ARRAY":
                    this.state = "END";
                    object.forEach(function (user) {
                        console.log(user.name);
                    });
         }
    }
}
(conn = new Conn()).open();


Persze ez csak egy modell, sok hibája van, többek közt a "this".
Ennek egy működő változatát letesztelten node.js/postgres alatt,
de mivel nem ismerem és nem is akarom megismerni a node.js-t így
nem igazán izgat, szinte biztos meg lehet oldani.

A lényeg, hogy áttekinthető és jól fejleszthető, mint úgy általában
az OOP megoldások.
34

:-)

MadBence · 2013. Jún. 28. (P), 20.26
Az ES6->ES5 fordítók valami ilyesmit csinálnak :). Ami nekem ebben nem tetszik, hogy minden forgatókönyvre meg kell írnom a saját switch szerkezetemet, nekem ez túl nagy overhead. Plusz ez (lényegében) ugyanaz mint a waterfall, csak kicsit hosszabb...
35

Bocs, de több dologban

Thor · 2013. Jún. 29. (Szo), 07.41
Bocs, de több dologban tévedsz..

Egyrészt a waterfall szintén a semmire sem jó kategóriába tartozik,
mert egy adott feladatra alkalmas, azontúl viszont használhatatlan a kód, főleg ott, ahol hibakezelést és több "szempontot" is fegyelembe kell venni. Szóval valós körülmények között nagyon-nagyon korlátozott a használata.

A másik, hogy a switch szerkezetet, csak egyszer kell megírni, mivel js-ben is van öröklődés, ezért a nagy overhead nem állja meg a helyét, max. a Te megoldásodnál, ahol valóban nagy overhead ugyanazt a kódot 100x leírni, egy komolyabb alkalmazásnál.

Harmadrész az adott model, egy kis fejlesztéssel korrektebbül megoldja a "Promise" problematikát.

Részemről én mindent elmondtam.
36

Örökléssel nem fogod a switch

MadBence · 2013. Jún. 29. (Szo), 14.32
Örökléssel nem fogod a switch szerkezet ágait egyesével babrálni, új case-ket hozzáadni. Cserébe az állapotot is neked kell karbantartani. A megoldásod megegyezik a waterfall modellel, mert:
  • Egymás után hajtasz végre aszinkron műveleteket
  • A callback függvény a következő lépést végrehajtó függvény
  • A hibakezelés ugyanolyan egyszerű (kényelmetlen)

Az én waterfall megvalósításomnál csak az egyes lépéseket megvalósító függvényeket kell megírni, ezt szerintem nem lehet kihagyni. (persze ha tudsz egy jó módszert, amivel a gép kitalálja helyettem, hogy mit akarok csinálni, szívesen meghallgatom)
Ahogy a cikkben is leírtam, az ilyen végtelenül egyszerű szituációkban teljesen korrekt megoldás az ilyen egyszerű aszinkron vezérlési szerkezetek alkalmazása, ha ennél több kell, akkor ott a Promise és többi lehetőség.
40

Tényleg utolsó hozzászólás a

Thor · 2013. Júl. 1. (H), 09.15
Tényleg utolsó hozzászólás a részemről.

Egy szóval sem említettem, hogy a switch-et lehet módosítani öröklődéssel és ne vedd zokon, de el kellene kezdened komolyabb nyelvekkel (C változatok, Java, stb..) is foglalkoznod mint a Javascript és nem csak ismerkedési szintig, ugyanis látszik a hozzászólásodból, hogy a béka segge alatt van OOP a téren a gyakorlati ismereted, tapasztalatod. De nincs ezzel baj, mindenki volt ezen a szinten, viszont ez az oka annak, hogy mi addig nem fogunk egyetérteni, és hogy nem érdemes rámutatnom az utolsó hozzászólásodban lévő "csacskaságokra".
A Promise meg szintén egy "végtelenül egyszerű szituációkban teljesen korrekt megoldás", nem szabad túlértékelni, csupán egy logikai model, minimális gyakorlati haszonnal.
Nem tudom még hogy alakul, de ha lesz időm a nyáron megírom a cikked 2.0-as változatát.
43

ha lesz időm a nyáron megírom

Hidvégi Gábor · 2013. Júl. 1. (H), 10.42
ha lesz időm a nyáron megírom a cikked 2.0-as változatát
Azok után, ahogy itt előadtad magad, az a minimum, hogy megírod, kedves germán istenség.
47

Írtál egy példakódot, melyről

MadBence · 2013. Júl. 1. (H), 22.36
Írtál egy példakódot, melyről azt állítottad, hogy az öröklődés segítségével testreszabható a működés, fölösleges kódírás (switch szerkezet) nélkül. Én ebből a példakódból nem igazán tudok rájönni, hogy ezt hogyan is kellene elképzelni. Másrészt az öröklődés arra használatos, hogy specializáljuk a működést, ami semmiképpen sem összeegyeztethető az említett testreszabással. Testreszabás alatt természetesen az eredeti problémára gondolok, azaz hogy nyújtson megoldást tetszőleges, callback alapú aszinkron API-t használó kódok problémájára. Ez egyértelműen nem az adatbázisos példa speciális esete, épp ellenkezőleg. Kíváncsi lennék, Te hogyan fogalmaznád meg ezt az absztrakt problémát (amire ezek szerint működő, hatékony, "tiszta" koncepciód/implementációd van).

A publikált kódoddal nekem továbbra is az a problémám, hogy:
  • Nehezen módosítható (az állapotokat Neked kell kezelned, új feldolgozási lépés beszúrása nehézkes (hiszen figyelni kell, hogy az előtte lévő állapotban a feldolgozás után a megfelelő állapotba kerüljünk).
  • Nem nyújt általános megoldást (illetve én nem látom, hogy egy absztraktabb problémára hogyan nézne ki a megvalósítás).


Használjunk állapotgépet ott, ahol állapotgépre van szükség. Egy ilyen lineáris műveletsorozatnál nincs erre szükség.

A véleményemmel, miszerint (többek között) a waterfall "egyszerű szituációkban teljesen korrekt megoldás", nem vagyok egyedül, elég csak megnézni, hogy az async a második leggyakoribb nodejs package dependencia.

Most ahhoz nem is szólnék hozzá, hogy a hozzászólásaimból és JavaScript példakódjaimból milyen következtetéseket tudsz leszűrni Java/C++/stb kódjaimra vonatkozóan :), ráadásul milyen hangnemben.
49

switch

Poetro · 2013. Júl. 1. (H), 23.56
A switch-től könnyen meg lehet szabadulni, és akkor az öröklődés is értelmet nyer:
var Conn = function() {};
Conn.prototype = {
    open: function() {
        this.state = "OPEN";
        database.open( this.process  );
    },
    process: function( error, object ) {
        if ( error )
            console.log( error );

        if (this.state in this.processHandler) {
            this.processHandler[this.state]();
        }
        else {
            throw new Error("Not implemented state");
        }
    },
    processHandler: {
        "OPEN": function () {
            this.state = "COLLECTION";
            object.collection('user', this.process );
        }
        "COLLECTION": function () {
            this.state = "FIND";
            object.find({age: {$gt: 18}}, this.process );
        }
        "FIND": function () {
            this.state = "TO_ARRAY";
            object.toArray( this.process );
        }
        "TO_ARRAY": function () {
            this.state = "END";
            object.forEach(function (user) {
                console.log(user.name);
            });
        }
    }
};
(conn = new Conn()).open();
50

Kellemesebb :). Viszont a kód

MadBence · 2013. Júl. 2. (K), 00.09
Kellemesebb :). Viszont a kód még mindig túl specifikus, az open hívás hatására szűrve kilistázódnak bizonyos felhasználók az adatbázisból. A függvény neve pedig nem ezt sugallja, az open nyisson egy adatbázis kapcsolatot, de ne tegyen mást.
51

Működés

Poetro · 2013. Júl. 2. (K), 00.14
A működéshez nem nyúltam, csak refaktoráltam az eredeti kódot, hogy kellemesebb legyen.
54

Abban mindenképp igazad van,

Thor · 2013. Júl. 2. (K), 10.40
Abban mindenképp igazad van, hogy kiemeltem az adatbázisos példád,
mert rossz, mint ahogy az ajax-os példa is az, mivel mind a kettő sokkal bonyolultabb story, és a promise és egyéb megoldások alkalmatlanok a megoldásukra.

Az adatbázisos példa abból a szempontból jó, hogy meg is lehet valósítani.
A problémám, amiért nem tudom Neked előhúzni a nyuszit a kalapból, hogy nem ismerem a node.js-t és már elkezdtem olvasgatni, mindig az egyik post jut az eszembe, aminek a lényege: "that shit works".

Idáig jutottam Postgres db-vel (működő kód):

hello_include.js

var pg = require('pg');

conString = "tcp://{USERNAME}:{PASSWORD}@localhost/{DATABASE}";

TConnection = function(){};
TConnection.prototype =
{
    open: function( )
    {
         this.state = 0;                                // Not connected
         this.client = new pg.Client( conString );
         this.client.connect( this.callback( this ) );
    },
    close: function()
    {
         this.client.end();
         this.client = null;
    },
    process: function( error, result )
    {
        if ( error )
        {
            this.error();
        }
        else
        {
            switch ( this.state )
            {
                case 0:                             // Not connected
                        this.state = 1;             // Connected
                        this.query();
                        break;
                case 1:// Process query response
                        this.state = 1;             // Query ok
                        this.response( result )
            }
        }
    },
    callback: function( _this_ )
    {
        return function( a, b )
        {
           _this_.process.call( _this_, a, b );
           _this_ = null;
        }
    }
};


hello.js

require('./hello_include.js');

getInstance = function ( superName, protoFunctions )
{
    var a = function(){}
    a.prototype = new superName();
    for ( fn in protoFunctions )
        a.prototype[fn] = protoFunctions[fn];
    return new a();
};

//app.get('/', function( request, response )
//{
    getInstance( TConnection,
    {
        query: function()
        {
           this.client.query('SELECT NOW() AS "theTime"', this.callback( this ) );
        },
        response: function( result )
        {
            console.log( result.rows[0].theTime );
            this.close();
        },
        error: function( error )
        {
            console.log( error );
            // Hibakezeles ..
        },
        timeout: function()
        {
           // Timeout
        }
    }).open( "request", "response" );
//});


Ebben a megvalósításban
a) nincs piramis szerkezet
b) 2-4 egymástól jól elszeparált függvényt kell csak megírni: a lényeget
c) fejleszthető, mert a fejlesztést elég szinte csak az inculde-t "osztályban"
elvégezni.

Ellenben biztos, hogy nem a legjobb, mert ahhoz sokkal több időt kellene a node.js megismerésével eltölteni, azaz lehet, hogy blokkol valahol, lehet bukik az egész valamely a node.js tulajdonság miatt, stb., viszont működik.

Ha nem tetszik az open(), nevezd át nyugodtan szörözésre()
28

A probléma inkább ott van,

Hidvégi Gábor · 2013. Jún. 28. (P), 07.59
A probléma inkább ott van, hogy miért használ valaki (2011-ben) sync-es hívást?
Lehetnek olyan webes alkalmazások, ahol az első kérés válaszának megérkezéséig nem szabad más, adatokat módosító eseménynek megtörténnie. Ezt a legegyszerűbb szinkron hívásokkal megtenni, bár valóban nem megbízható, azaz szerveroldalon is meg kell oldani.
32

Így kellett volna írnod:

Thor · 2013. Jún. 28. (P), 19.23
Így kellett volna írnod: "azaz szerveroldalon kell megoldani"

Amennyiben valaki olyan webes alkalmazást akar írni,
ahol más esemény nem történhet meg, azt szerver oldalon kell megoldania és csakis ott,
mert a kliens oldalt nem lehet teljesen "megbízhatóvá" tenni.
37

Nem csakis

Szigyártó Mihály · 2013. Jún. 30. (V), 10.33
"Kényelmi" szempontból szerintem a kliens oldalon is lehet erre megoldás, a megbízhatóság nem módosul attól, hogy te csinálsz valamit a kliens oldalon, természetesen a szerver oldalon is kell vele foglalkozni.

[off]
Egyébként még mindig nem sikerült nem tulok módon megfogalmaznod, amit akartál mondani, bár nem is látszik túlságosan az igyekezet.
[/off]
38

Tény, hogy kliens oldalon is

Thor · 2013. Júl. 1. (H), 07.57
Tény, hogy kliens oldalon is foglalkozni vele megfelelő mértékig, de kliens oldalon egyelőre még nem tudod kiküszöbölni 100 százalékban, hogy ugyanazzal a session és egyéb azonosítóval ne lehessen számtalan módon "meghekkelni" a kliens oldalt.
Szóval, ha hiszel a tündérmesékben és abban, hogy a kliens oldal megbízható, akkor kliens oldali megoldást alkalmazol.
39

Senki sem mondta, hogy az

Hidvégi Gábor · 2013. Júl. 1. (H), 08.13
Senki sem mondta, hogy az adott funkciót kizárólag kliensoldalon kell megoldani.
41

Olvasd el az első mondatát

Thor · 2013. Júl. 1. (H), 09.16
Olvasd el az első mondatát légy szíves, annak a hozzászólásnak amire reagáltam. Kösz.
42

Te is, és lehetőleg a

Hidvégi Gábor · 2013. Júl. 1. (H), 09.21
Te is, és lehetőleg a kétbetűs szavakra koncentrálj (és azokon belül nem a "te"-re).
45

.. tetszik a cikked, egy

Hidvégi Gábor · 2013. Júl. 1. (H), 17.19
.. tetszik a cikked, egy problémám van vele, hogy a végeredmény, amit kihoztál belőle a végén is szörnyű. A legnagyobb probléma vele, hogy teljesen átláthatatlan, fejleszthetetlen az egész. Tökéletes arra, ha egy cégnél olyan kódot akarsz írni, amit rajtad kívül senki nem "ért", csak pár óra "forráskód" böngészés után és kvázi "kirúghatatlan" leszel tőle.
Tipikus javascriptes agymenés, amivel tele van a net
Tökéletesen egyetértek. Igazából ez az egész aszinkronosdi meg "nem blokkoló" csak arra jó, hogy összezavarja az egyszeri programozót, és ilyen kérdéseket tegyen fel:
Például adott egy for ciklus ami a példának okáért mondjuk sima összeadásokat végez egy tömbön. Ezt érdemes-e aszinkronra ki szervezni?
46

Funkcionálhat egyfajta

Joó Ádám · 2013. Júl. 1. (H), 18.11
Funkcionálhat egyfajta szakmai szűrőként :)
9

Köszi a fibers-ért, ezt nem

virág · 2013. Jún. 25. (K), 21.24
Köszi a fibers-ért, ezt nem ismertem :) Jó írás!
21

Promises

Poetro · 2013. Jún. 26. (Sze), 16.34
Nem tudom, mennyire kapcsolódik ide, de valamikor idén tartottam előadást a Promise rendszerekről. Annak diái elérhetők. Hátha van közötte valami, ami ebben a remek cikkben nem lett említve.
44

Blokkolás

Hidvégi Gábor · 2013. Júl. 1. (H), 10.47
Sajnos az SJAX hívásunk blokkolni fog (...). Ha a csillagok megfelelően állnak, akkor persze ez nagyon rövid időintervallum (néhányszor 10 ms), de ha épp nem optimális a hálózat, akkor ez akár másodpercekig is eltarthat. Ez a felhasználói élmény szempontjából roppant előnytelen.
Webes alapú alkalmazásnál nem teljesen mindegy, hogy blokkol-e vagy sem, ha lassú kapcsolat esetén a következő művelet elvégzéséhez is a szerverhez kell fordulni?
48

A szinkron hívás hátránya az,

MadBence · 2013. Júl. 1. (H), 23.03
A szinkron hívás hátránya az, hogy közben a felhasználói felület nem működik. Nem tudsz kattintani, szöveget kijelölni, minden interakció blokkolva van, hiszen a vezérlés várakozik (persze passzívan) a szinkron hívásnál. Egyszerű példa (ez most nodejs, pl PHP-ban a szerveroldali setTimeout helyett sleep használható), a lényeg: nem engedhető meg az alkalmazás szempontjából az ilyen jellegű "fagyás".
var http = require('http');

var fun = function() {
    var i = 0;
    var foo = document.getElementById('foo');
    setInterval(function() {
        foo.value = i++;
    }, 100);
    document.getElementById('sync').onclick = function() {
        var a = new XMLHttpRequest();
        a.open('GET', '/foo', false);
        a.send();
    }
    document.getElementById('async').onclick = function() {
        var a = new XMLHttpRequest();
        a.open('GET', '/foo', true);
        a.send();
    }
}

http.createServer(function(req, res){
    if(req.url != '/') {
        return setTimeout(function() {
            res.end('OK');
        }, 5000)
    }
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('<button>Hello world!</button>\
             <button id="sync">Szinkron</button>\
             <button id="async">Aszinkron</button>\
             <a href="#">Link</a><input id="foo" />\
             <script>('+fun.toString()+')();</script>');
}).listen(80);
52

Egyedül mobilneten tudok

Hidvégi Gábor · 2013. Júl. 2. (K), 06.41
Egyedül mobilneten tudok elképzelni olyan esetet, ahol lassú a kapcsolat nagyobb adatcsomag küldésekor, alap ADSL-en egy átlagkérésre a válasz pár tizedmásodperc alatt megjön.
53

Attól függetlenül, hogy ez a

MadBence · 2013. Júl. 2. (K), 08.21
Attól függetlenül, hogy ez a kiesés sem nagyon engedhető meg (hiszen pl a google saját statisztikái szerint néhány tizedmásodperccel nagyobb késleltetés mellett a forgalom 20%-kal csökken, sajnos nem találtam meg az eredeti cikket erről), tegyük fel, hogy nincs hatással a UX-re. Garantált a válaszidő? A válasz természetesen az hogy nem, hiszen a hálózat bármely eleme bemondhatja az unalmast. Ilyenkor nincs mit tenni, amíg az operációs rendszer ki nem találja, hogy mi történt (timeoutol, stb), addig le van fagyva az egész oldal. Ez nagyon frusztráló tud lenni a felhasználó számára, semmiképpen sem engedhető meg.
55

milyen gyakorlati esetekben használhatóak a fentiek?

EL Tebe · 2013. Júl. 2. (K), 10.43
Js terén csak szögegyszerű dolgokat szoktam írni egy-egy weblaphoz, de jó lenne elmélyedni benne. A cikk nyilván haladó témakörbe tartozik, azonban szeretném megérteni. Légyszi segítsetek:

A szinkron működés miatt az eseménykezelő ciklusnak esélye sincs a futásra, tehát blokkolni fog.


0) Azt nem értem, hogyha valami váratlan hibára fut mondjuk egy DB lekérdezést futtató esemény, akkor miért jó az, ha a kódunk tovább fut (legalábbis az egymásra épülő részek esetében)
Vagy ez a probléma nem egy adott modellt érint (pl db lekérdezés+kiíratás) hanem minden mást is (a teljes alkalmazást?) Pl a "lekérdezés eredményét megjelenítő popuplayer "bezáró" gombjának eseménykezelőjét is?


1) Milyen esetben jó és rossz ez a megoldás? (1-2 gyakorlati példa?)
Azért is kérdezem, mert a hozzászólások elég ellentmondásosak, elbizonytalanítottak (ráadásul tele vannak személyeskedéssel)
63

Próbáld ki ezt debug-ra:

inf · 2013. Júl. 2. (K), 13.23
Próbáld ki ezt debug-ra: https://github.com/dannycoates/node-inspector. Egyelőre én php + js klienst írok, úgyhogy nem igazán tudok segíteni ilyen téren. Később belefogok majd a nodejs-be, ha lesz olyan projektem, vagy csak próbálgatás miatt.

Azt nem értem, hogyha valami váratlan hibára fut mondjuk egy DB lekérdezést futtató esemény, akkor miért jó az, ha a kódunk tovább fut (legalábbis az egymásra épülő részek esetében)


Ez egyáltalán nem igaz, bármelyik aszinkron sequence megoldást használod, mindegyiknél az van, hogy hiba esetén nem küldi el a következő kérést, hanem ugyanúgy elszáll. A párhuzamos kéréseknél meg lehet oldani, hogyha az egyik hibázik, akkor az összes többit leállítsa a rendszer, legalábbis ahol erre lehetőség van. Nyilván nem fogsz párhuzamosan select-en kívül más típusú kéréseket küldeni a szervernek. Ha adatbázissal kommunikálsz, akkor elég nyilvánvaló, hogy insert, update, delete esetében tranzakcióban használod sorosan. Én postgresql-t tolok most, és tárolt eljárásba teszek gyakorlatilag mindent, mert nekem úgy tisztább...

Milyen esetben jó és rossz ez a megoldás? (1-2 gyakorlati példa?)


Olyan esetben jobbak az aszinkron kérések, ahol párhuzamos kérésekkel jobb eredményt tudsz elérni. Pl van több kérésed, és mindegyik egyenként sok időbe telik. Ha ezeket sorban futtatod, akkor az idők összeadódnak, ezzel szemben, ha párhuzamosan, akkor csak a leghosszabb ideig tartó kérésnek megfelelő időt kell kivárni.

Ha valamilyen alkalmazással szerver oldali eseményeket akarsz implementálni, akkor nodejs alapból jobb megoldás, mert eleve események vannak a szerveren, nem kell külön implementálni őket, mint mondjuk egy szinkron nyelv esetében. Tehát pl a munkamenet kezelés jóval egyszerűbb lesz benne. Ugyanígy bármilyen adat stream-elés sokkal egyszerűbb, ha történetesen socket.io-s kliens és a szerver között folyik az adat, akkor gyakorlatilag ugyanazzal az interface-el lesz dolgod szerver oldalon is nodejs esetében, nem lesz semmi új... Például chat alkalmazások, audio/video stream, realtime online játékok, stb... esetében ez mind nagyon jó.

Ha hagyományos weblapokat csinálsz, sima alkalmazásokat, ahol a kliens oldalon inkább csak html van, akkor jobb megoldás a php használata, mert nodejs-el körülményesebb lesz. Ezeknél nincs igazán szükség a nodejs előnyeire, viszont az aszinkron kérések miatt bonyolultabb lesz a kódod.
66

+1

EL Tebe · 2013. Júl. 2. (K), 14.32
köszönöm, így már sokkal tisztább
67

Vagy ez a probléma nem egy

Joó Ádám · 2013. Júl. 2. (K), 18.27
Vagy ez a probléma nem egy adott modellt érint (pl db lekérdezés+kiíratás) hanem minden mást is (a teljes alkalmazást?) Pl a "lekérdezés eredményét megjelenítő popuplayer "bezáró" gombjának eseménykezelőjét is?


Kicsit zavaros, de ha jól értem, a válasz igen. Az egész alkalmazás egyetlen szálon fut, így ha éppen IO-ra várakozik, akkor addig semmilyen más eseményre nem tud reagálni.

Milyen esetben jó és rossz ez a megoldás? (1-2 gyakorlati példa?)


Az aszinkronitás? Jó grafikus felületeknél, ahol természetes az eseményközpontú megközelítés. Szerveralkalmazásoknál, ha a várható terhelés akkora, hogy többszálú megvalósításban a szálak száma miatt elfogyna a RAM, jelenleg rákényszerülhetsz erre a megoldásra, azonban jó nem lesz, mert IO-t esemény alapon csinálni nehezen követhető kódhoz vezet.
56

Tetszik a cikk, nagyon

inf · 2013. Júl. 2. (K), 12.01
Tetszik a cikk, nagyon átfogó. Én nem hiszem, hogy olyan nagyon új dolgok kijönnének még a témában. Körülbelül egy éve is ugyanitt tartottunk...

A yield terén még van ilyen: https://github.com/laverdet/node-fibers, thread témában meg ilyen: https://github.com/xk/node-threads-a-gogo (ez mondjuk ritkábban frissül, ahogy nézem).
68

Dehogynem!

MadBence · 2013. Júl. 2. (K), 18.31
A fejlődés ütemére szerintem jó példa, hogy nemsokkal a cikk megjelenése előtt debütált a v8-ban a yield és társai, így Chrome Canary változatában pl már elérhető, ugyanígy a 0.11-es node.js-ben is használható már. A fiberset én is megemlítettem, de szerintem nagyon hekkelős, szóval nem is nagyon foglalkoztam vele. A gogo eszembe sem jutott :), de szerintem ezek az ötletek eleve halva születtek. Amíg nincsenek nyelvi szinten támogatva szálak, addig nem érdemes utánozni őket, csak félreértések származnak belőlük.
70

Én is inkább afelé hajlok,

inf · 2013. Júl. 3. (Sze), 10.37
Én is inkább afelé hajlok, hogy az aszinkron dolgokat, szálakat, stb... Nyelvi szinten kellene támogatni. Ha a másik oldalról nézzük a dolgot, akkor elég jól mutatja, hogy mennyire rugalmas a nodejs, hogy ezeket a dolgokat c-ben leprogramozott modulokként bele lehet szórni.
71

Szálak

Poetro · 2013. Júl. 3. (Sze), 10.57
Ahol vannak szálak, ott általában csak problémát okoznak, és jóval bonyolultabb megvalósítást eredményeznek. A szálak felülírhatják a megosztott erőforrásokat, változókat, ezzel inkonzisztens állapotba hozva egymást. Ezt kikerülni nem egyszerű probléma. Próbáltál már Java-ban, vagy más nyelvben, ahol vannak szálak, többszálú programozást csinálni? Elárulom, az Event Loop jelentősen egyszerűbbé teszi a dolgot. Ráadásul Node.js-ben vannak domain-ek, valamint fork-olhatod az alkalmazásodat, és Unix socketekkel kommunikálhatsz közöttük.
73

Ja tisztában vagyok vele,

inf · 2013. Júl. 3. (Sze), 11.03
Ja tisztában vagyok vele, hogy az event loop-ot arra találták ki nodejs-ben, hogy megoldják a szálakkal kapcsolatos problémákat. Láttam már szálakat használó kódot (az konkrétan androidra volt), én nem írtam még soha. Abban viszont biztos vagyok, hogy a megfelelő fejlesztési módszerrel szálakkal is lehet jó minőségű kódot csinálni. Ez is olyan dolog, hogy érteni kell hozzá.

Js-ben amúgy a webworker is valami hasonló, mint a szálak, ha jól tudom:
http://ejohn.org/blog/web-workers/
74

Nem egészen

complex857 · 2013. Júl. 3. (Sze), 11.32
Web workerek nem hordozzák a többszálú programok megosztott adatokhoz kapcsolódó problémáit azon egyszerű oknak fogva, hogy nem tudnak adatokat megosztani a "szülő" folyamattal. Tudsz neki adatokat küldeni (szokásos eventek), ő meg egyszer csak visszatér valami eredménnyel (generál eventet szülőnél), de futás közben egyik fél se piszkálhat bele másik dolgaiba (nem érik el DOM-ot, illetve úgy általában majdnem semmit browserből)
Ez abszolút szándékosan lett így tervezve pontosan azért, hogy ne keletkezzenek a szokásos szálkezelés körüli bonyodalmak a megosztott adatokkal.
75

Azért az adathozzáférés

Joó Ádám · 2013. Júl. 3. (Sze), 12.26
Azért az adathozzáférés szinkronizálására van más módszer is, mint okafogyottá tenni. Használhatsz állandó adatstruktúrákat, használhatsz szinkronizált adatstruktúrákat, használhatsz üzenetküldést…

A többszálú, szinkron megközelítés sokkal természetesebb, mint a Node.js-é.

(A fork és az IPC igen drága, így nem reális alternatíva az általános esetben, ráadásul nem küldhetsz át bármit socketen. Volt szerencsém socket leírókat átadni socketen keresztül, körülbelül egy hetem ment rá.)