Jak uniknąć długiego zagnieżdżania funkcji asynchronicznych w Node.js.


158

Chcę utworzyć stronę wyświetlającą niektóre dane z bazy danych, więc utworzyłem kilka funkcji, które pobierają te dane z mojej bazy danych. Jestem tylko nowicjuszem w Node.js, więc o ile rozumiem, jeśli chcę użyć ich wszystkich na jednej stronie (odpowiedź HTTP), musiałbym je wszystkie zagnieździć:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  getSomeDate(client, function(someData) {
    html += "<p>"+ someData +"</p>";
    getSomeOtherDate(client, function(someOtherData) {
      html += "<p>"+ someOtherData +"</p>";
      getMoreData(client, function(moreData) {
        html += "<p>"+ moreData +"</p>";
        res.write(html);
        res.end();
      });
    });
  });

Jeśli takich funkcji jest wiele, zagnieżdżanie staje się problemem .

Czy jest sposób, aby tego uniknąć? Myślę, że ma to związek ze sposobem łączenia wielu funkcji asynchronicznych, co wydaje się być czymś fundamentalnym.


12
Więc kiedy masz 10 funkcji asynchronicznych, masz 10 poziomów wcięć?
Kay Pale


1
Kolejny problem: wstawienie innej funkcji między getSomeDatei getSomeOtherDatekończy się zmianą wcięcia wielu wierszy, co sprawia, że ​​historia git jest trudniejsza do odczytania ( git blamejest nawet bezużyteczna po tym) i prawdopodobnie robisz błędy, robiąc to ręcznie
Daniel Alder

Odpowiedzi:


73

Ciekawa obserwacja. Zauważ, że w JavaScript można normalnie zamienić wbudowane anonimowe funkcje zwrotne na nazwane zmienne funkcyjne.

Następujące:

http.createServer(function (req, res) {
   // inline callback function ...

   getSomeData(client, function (someData) {
      // another inline callback function ...

      getMoreData(client, function(moreData) {
         // one more inline callback function ...
      });
   });

   // etc ...
});

Można przepisać, aby wyglądał mniej więcej tak:

var moreDataParser = function (moreData) {
   // date parsing logic
};

var someDataParser = function (someData) {
   // some data parsing logic

   getMoreData(client, moreDataParser);
};

var createServerCallback = function (req, res) {
   // create server logic

   getSomeData(client, someDataParser);

   // etc ...
};

http.createServer(createServerCallback);

Jeśli jednak nie planujesz ponownie używać logiki wywołań zwrotnych w innych miejscach, często łatwiej jest odczytać wbudowane funkcje anonimowe, jak w twoim przykładzie. Dzięki temu nie będziesz musiał szukać nazwy dla wszystkich wywołań zwrotnych.

Ponadto zauważ, że jak @pst zauważył w komentarzu poniżej, jeśli uzyskujesz dostęp do zmiennych zamykających w ramach funkcji wewnętrznych, powyższe nie byłoby prostym tłumaczeniem. W takich przypadkach korzystanie z wbudowanych funkcji anonimowych jest jeszcze bardziej korzystne.


26
Jednak (i ​​naprawdę po to, aby zrozumieć kompromis), gdy nie jest zagnieżdżony, niektóre semantyki zamknięcia zmiennych mogą zostać utracone, więc nie jest to bezpośrednie tłumaczenie. W powyższym przykładzie getMoreDatautracono dostęp do „res” .

2
Myślę, że twoje rozwiązanie jest zepsute: someDataParserfaktycznie analizuje WSZYSTKIE dane, ponieważ również wywołuje getMoreData. W tym sensie nazwa funkcji jest nieprawidłowa i okazuje się, że w rzeczywistości nie usunęliśmy problemu z zagnieżdżeniem.
Konstantin Schubert

63

Dobrze, po prostu użyj jednego z tych modułów.

Okaże się to:

dbGet('userIdOf:bobvance', function(userId) {
    dbSet('user:' + userId + ':email', 'bobvance@potato.egg', function() {
        dbSet('user:' + userId + ':firstName', 'Bob', function() {
            dbSet('user:' + userId + ':lastName', 'Vance', function() {
                okWeAreDone();
            });
        });
    });
});

Zaangażowany w to:

flow.exec(
    function() {
        dbGet('userIdOf:bobvance', this);

    },function(userId) {
        dbSet('user:' + userId + ':email', 'bobvance@potato.egg', this.MULTI());
        dbSet('user:' + userId + ':firstName', 'Bob', this.MULTI());
        dbSet('user:' + userId + ':lastName', 'Vance', this.MULTI());

    },function() {
        okWeAreDone()
    }
);

9
Rzuciłem okiem na flow-js, step i async i wydaje się, że zajmują się one tylko kolejnością wykonywania funkcji. W moim przypadku w każdym wcięciu jest dostęp do zmiennych zamykających. Na przykład funkcje działają w ten sposób: pobierz żądanie HTTP / res, pobierz identyfikator użytkownika z DB dla pliku cookie, pobierz e-mail z późniejszym identyfikatorem użytkownika, pobierz więcej danych dla późniejszego e-maila, ..., pobierz X dla późniejszego Y, ... Jeśli się nie mylę, te frameworki zapewniają tylko, że funkcje asynchroniczne będą wykonywane we właściwej kolejności, ale w każdej treści funkcji nie ma sposobu, aby uzyskać zmienną dostarczaną naturalnie przez domknięcia (?) Dzięki :)
Kay Pale

9
Jeśli chodzi o ranking tych bibliotek, sprawdziłem liczbę "gwiazdek" w każdej z nich na Github. async ma najwięcej z około 3000, Step jest następny z około 1000, inne są znacznie mniejsze. Oczywiście nie wszyscy robią to samo :-)
kgilpin

3
@KayPale Zwykle używam async.waterfall i czasami będę mieć własne funkcje dla każdego etapu / kroku, które będą przekazywać to, czego potrzebuje następny krok, lub definiuję zmienne przed wywołaniem async.METHOD, aby były dostępne w dół. Użyje również METHODNAME.bind (...) dla moich wywołań async. *, Co też działa całkiem nieźle.
Tracker1

Szybkie pytanie: czy na liście modułów ostatnie dwa są takie same? Tj. „Async.js” i „async”
dari0h,

18

W większości zgadzam się z Danielem Vassallo. Jeśli potrafisz rozbić skomplikowaną i głęboko zagnieżdżoną funkcję na osobne nazwane funkcje, to zazwyczaj jest to dobry pomysł. W sytuacjach, w których warto zrobić to w ramach jednej funkcji, możesz użyć jednej z wielu dostępnych bibliotek asynchronicznych node.js. Ludzie wymyślili wiele różnych sposobów rozwiązania tego problemu, więc spójrz na stronę modułów node.js i zobacz, co myślisz.

Sam napisałem do tego moduł o nazwie async.js . Korzystając z tego, powyższy przykład można zaktualizować do:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  async.series({
    someData: async.apply(getSomeDate, client),
    someOtherData: async.apply(getSomeOtherDate, client),
    moreData: async.apply(getMoreData, client)
  },
  function (err, results) {
    var html = "<h1>Demo page</h1>";
    html += "<p>" + results.someData + "</p>";
    html += "<p>" + results.someOtherData + "</p>";
    html += "<p>" + results.moreData + "</p>";
    res.write(html);
    res.end();
  });
});

Jedną z fajnych rzeczy w tym podejściu jest to, że możesz szybko zmienić swój kod, aby równolegle pobierać dane, zmieniając funkcję „szereg” na „równolegle”. Co więcej, async.js będzie również działać w przeglądarce, więc możesz użyć tych samych metod, co w node.js, jeśli napotkasz jakiś skomplikowany kod asynchroniczny.

Mam nadzieję, że to przydatne!


Cześć Caolan i dzięki za odpowiedź! W moim przypadku w każdym wcięciu jest dostęp do zmiennych zamykających. Na przykład funkcje działają w ten sposób: pobierz żądanie HTTP / res, pobierz identyfikator użytkownika z DB dla pliku cookie, pobierz e-mail z późniejszym identyfikatorem użytkownika, pobierz więcej danych dla późniejszego e-maila, ..., pobierz X dla późniejszego Y, ... Jeśli się nie mylę, kod, który sugerujesz, zapewnia jedynie, że funkcje asynchroniczne będą wykonywane we właściwej kolejności, ale w każdej treści funkcji nie ma sposobu, aby uzyskać zmienną dostarczaną naturalnie przez domknięcia w moim oryginalnym kodzie. Czy tak jest?
Kay Pale,

3
To, co próbujesz osiągnąć, jest architektonicznie nazywane potokiem danych. W takich przypadkach możesz użyć wodospadu asynchronicznego.
Rudolf Meijering

18

Możesz użyć tej sztuczki z tablicą zamiast zagnieżdżonych funkcji lub modułu.

O wiele łatwiejsze dla oczu.

var fs = require("fs");
var chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step3");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step4");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step5");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("done");
    },
];
chain.shift()();

Możesz rozszerzyć idiom na równoległe procesy lub nawet równoległe łańcuchy procesów:

var fs = require("fs");
var fork1 = 2, fork2 = 2, chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        var next = chain.shift();
        fs.stat("f2a.js",next);
        fs.stat("f2b.js",next);
    },
    function(err, stats) {
        if ( --fork1 )
            return;
        console.log("step3");
        var next = chain.shift();

        var chain1 = [
            function() { 
                console.log("step4aa");
                fs.stat("f1.js",chain1.shift());
            },
            function(err, stats) { 
                console.log("step4ab");
                fs.stat("f1ab.js",next);
            },
        ];
        chain1.shift()();

        var chain2 = [
            function() { 
                console.log("step4ba");
                fs.stat("f1.js",chain2.shift());
            },
            function(err, stats) { 
                console.log("step4bb");
                fs.stat("f1ab.js",next);
            },
        ];
        chain2.shift()();
    },
    function(err, stats) {
        if ( --fork2 )
            return;
        console.log("done");
    },
];
chain.shift()();

15

W tym celu bardzo lubię async.js .

Problem rozwiązuje polecenie wodospad:

wodospad (zadania, [oddzwonienie])

Uruchamia tablicę funkcji w serii, z których każda przekazuje swoje wyniki do następnej w tablicy. Jeśli jednak którakolwiek z funkcji przekaże błąd do wywołania zwrotnego, następna funkcja nie zostanie wykonana, a główne wywołanie zwrotne zostanie natychmiast wywołane z błędem.

Argumenty

zadania - tablica funkcji do uruchomienia, każda funkcja otrzymuje wywołanie zwrotne (błąd, wynik1, wynik2, ...), które musi wywołać po zakończeniu. Pierwszy argument to błąd (który może mieć wartość null), a wszelkie dalsze argumenty zostaną przekazane jako argumenty w celu wykonania następnego zadania. callback (err, [results]) - Opcjonalne wywołanie zwrotne uruchamiane po zakończeniu wszystkich funkcji. Zostanie przekazane wyniki wywołania zwrotnego ostatniego zadania.

Przykład

async.waterfall([
    function(callback){
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
        callback(null, 'three');
    },
    function(arg1, callback){
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
    // result now equals 'done'    
});

Jeśli chodzi o zmienne req, res, będą one współdzielone w tym samym zakresie, co funkcja (req, res) {}, która obejmuje całe wywołanie async.waterfall.

Nie tylko to, async jest bardzo czysty. Chodzi mi o to, że zmieniam wiele przypadków takich jak ta:

function(o,cb){
    function2(o,function(err, resp){
        cb(err,resp);
    })
}

Najpierw:

function(o,cb){
    function2(o,cb);
}

Następnie do tego:

function2(o,cb);

Następnie do tego:

async.waterfall([function2,function3,function4],optionalcb)

Pozwala również na bardzo szybkie wywołanie wielu gotowych funkcji przygotowanych do asynchronizacji z pliku util.js. Po prostu połącz to, co chcesz zrobić, upewnij się, że o, cb jest obsługiwane uniwersalnie. To bardzo przyspiesza cały proces kodowania.


11

Potrzebujesz trochę cukru syntaktycznego. Sprawdź to:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = ["<h1>Demo page</h1>"];
  var pushHTML = html.push.bind(html);

  Queue.push( getSomeData.partial(client, pushHTML) );
  Queue.push( getSomeOtherData.partial(client, pushHTML) );
  Queue.push( getMoreData.partial(client, pushHTML) );
  Queue.push( function() {
    res.write(html.join(''));
    res.end();
  });
  Queue.execute();
}); 

Całkiem fajnie , prawda? Możesz zauważyć, że html stał się tablicą. Dzieje się tak częściowo dlatego, że ciągi są niezmienne, więc lepiej jest buforować dane wyjściowe w tablicy, niż odrzucać coraz większe ciągi. Innym powodem jest inna ładna składnia z bind.

Queuew przykładzie jest tak naprawdę tylko przykładem i razem z partialmożna zaimplementować w następujący sposób

// Functional programming for the rescue
Function.prototype.partial = function() {
  var fun = this,
      preArgs = Array.prototype.slice.call(arguments);
  return function() {
    fun.apply(null, preArgs.concat.apply(preArgs, arguments));
  };
};

Queue = [];
Queue.execute = function () {
  if (Queue.length) {
    Queue.shift()(Queue.execute);
  }
};

1
Queue.execute () po prostu wykona części składowe jedna po drugiej, bez czekania na wyniki wywołań asynchronicznych.
ngn

Dobrze, dzięki. Zaktualizowałem odpowiedź. Oto test: jsbin.com/ebobo5/edit (z opcjonalną lastfunkcją)
gblazex

Cześć galambalazs i dzięki za odpowiedź! W moim przypadku w każdym wcięciu jest dostęp do zmiennych zamykających. Na przykład funkcje działają w ten sposób: pobierz żądanie HTTP / res, pobierz identyfikator użytkownika z DB dla pliku cookie, pobierz e-mail z późniejszym identyfikatorem użytkownika, pobierz więcej danych dla późniejszego e-maila, ..., pobierz X dla późniejszego Y, ... Jeśli się nie mylę, kod, który sugerujesz, zapewnia jedynie, że funkcje asynchroniczne będą wykonywane we właściwej kolejności, ale w każdej treści funkcji nie ma sposobu, aby uzyskać zmienną dostarczaną naturalnie przez domknięcia w moim oryginalnym kodzie. Czy tak jest?
Kay Pale,

1
Cóż, na pewno tracisz domknięcia we wszystkich odpowiedziach. To, co możesz zrobić, to utworzyć obiekt o zasięgu globalnym dla udostępnianych danych. Na przykład twoja pierwsza funkcja dodaje, obj.emaila następna używa, obj.emaila potem ją usuwa (lub po prostu przypisuje null).
gblazex,

7

Jestem zakochany w Async.js odkąd go znalazłem. Ma async.seriesfunkcję, której możesz użyć, aby uniknąć długiego zagnieżdżania.

Dokumentacja:-


seria (zadania, [oddzwonienie])

Uruchom tablicę funkcji szeregowo, z których każda działa po zakończeniu poprzedniej funkcji. […]

Argumenty

tasks- Tablica funkcji do uruchomienia, każda funkcja otrzymuje wywołanie zwrotne, które musi wywołać po zakończeniu. callback(err, [results])- Opcjonalne wywołanie zwrotne do uruchomienia po zakończeniu wszystkich funkcji. Ta funkcja pobiera tablicę wszystkich argumentów przekazanych do wywołań zwrotnych używanych w tablicy.


Oto jak możemy zastosować to do twojego przykładowego kodu: -

http.createServer(function (req, res) {

    res.writeHead(200, {'Content-Type': 'text/html'});

    var html = "<h1>Demo page</h1>";

    async.series([
        function (callback) {
            getSomeData(client, function (someData) { 
                html += "<p>"+ someData +"</p>";

                callback();
            });
        },

        function (callback) {
            getSomeOtherData(client, function (someOtherData) { 
                html += "<p>"+ someOtherData +"</p>";

                callback(); 
            });
        },

        funciton (callback) {
            getMoreData(client, function (moreData) {
                html += "<p>"+ moreData +"</p>";

                callback();
            });
        }
    ], function () {
        res.write(html);
        res.end();
    });
});

6

Najprostszym cukrem syntaktycznym, jaki widziałem, jest obietnica węzłów.

npm zainstaluj obietnicę węzła || klon git https://github.com/kriszyp/node-promise

Używając tego, możesz łączyć metody asynchroniczne jako:

firstMethod().then(secondMethod).then(thirdMethod);

Wartość zwracana każdego z nich jest dostępna jako argument w następnym.


3

To, co tam zrobiłeś, polega na wzięciu wzorca asynchronicznego i zastosowaniu go do 3 funkcji wywoływanych po kolei, z których każda czeka na zakończenie poprzedniej przed rozpoczęciem - tj. Uczyniłeś je synchronicznymi . Problem w programowaniu asynchronicznym polega na tym, że możesz mieć kilka funkcji uruchomionych jednocześnie i nie musisz czekać na zakończenie każdej z nich.

jeśli getSomeDate () nie dostarcza niczego do getSomeOtherDate (), co nie dostarcza niczego do getMoreData (), to dlaczego nie wywołujesz ich asynchronicznie, jak na to pozwala js, lub jeśli są współzależne (a nie asynchroniczne), zapisz je jako pojedyncza funkcja?

Nie musisz używać zagnieżdżania do kontrolowania przepływu - na przykład, zakończ każdą funkcję, wywołując wspólną funkcję, która określa, kiedy wszystkie 3 zostały zakończone, a następnie wysyła odpowiedź.


2

Załóżmy, że możesz to zrobić:

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    var html = "<h1>Demo page</h1>";
    chain([
        function (next) {
            getSomeDate(client, next);
        },
        function (next, someData) {
            html += "<p>"+ someData +"</p>";
            getSomeOtherDate(client, next);
        },
        function (next, someOtherData) {
            html += "<p>"+ someOtherData +"</p>";
            getMoreData(client, next);
        },
        function (next, moreData) {
            html += "<p>"+ moreData +"</p>";
            res.write(html);
            res.end();
        }
    ]);
});

Wystarczy zaimplementować metodę chain (), aby częściowo stosowała każdą funkcję do następnej i natychmiast wywoływała tylko pierwszą funkcję:

function chain(fs) {
    var f = function () {};
    for (var i = fs.length - 1; i >= 0; i--) {
        f = fs[i].partial(f);
    }
    f();
}

Cześć ngn i dzięki za odpowiedź! W moim przypadku w każdym wcięciu jest dostęp do zmiennych zamykających. Na przykład funkcje działają w ten sposób: pobierz żądanie HTTP / res, pobierz identyfikator użytkownika z DB dla pliku cookie, pobierz e-mail z późniejszym identyfikatorem użytkownika, pobierz więcej danych dla późniejszego e-maila, ..., pobierz X dla późniejszego Y, ... Jeśli się nie mylę, kod, który sugerujesz, zapewnia jedynie, że funkcje asynchroniczne będą wykonywane we właściwej kolejności, ale w każdej treści funkcji nie ma sposobu, aby uzyskać zmienną dostarczaną naturalnie przez domknięcia w moim oryginalnym kodzie. Czy tak jest?
Kay Pale,

2

piekło zwrotne można łatwo uniknąć w czystym javascript z zamknięciem. poniższe rozwiązanie zakłada, że ​​wszystkie wywołania zwrotne są zgodne z sygnaturą funkcji (błąd, dane).

http.createServer(function (req, res) {
  var modeNext, onNext;

  // closure variable to keep track of next-callback-state
  modeNext = 0;

  // next-callback-handler
  onNext = function (error, data) {
    if (error) {
      modeNext = Infinity;
    } else {
      modeNext += 1;
    }
    switch (modeNext) {

    case 0:
      res.writeHead(200, {'Content-Type': 'text/html'});
      var html = "<h1>Demo page</h1>";
      getSomeDate(client, onNext);
      break;

    // handle someData
    case 1:
        html += "<p>"+ data +"</p>";
        getSomeOtherDate(client, onNext);
        break;

    // handle someOtherData
    case 2:
      html += "<p>"+ data +"</p>";
      getMoreData(client, onNext);
      break;

    // handle moreData
    case 3:
      html += "<p>"+ data +"</p>";
      res.write(html);
      res.end();
      break;

    // general catch-all error-handler
    default:
      res.statusCode = 500;
      res.end(error.message + '\n' + error.stack);
    }
  };
  onNext();
});

1

Niedawno stworzyłem prostszą abstrakcję o nazwie wait.for do wywoływania funkcji asynchronicznych w trybie synchronizacji (opartej na Fibers). Jest na wczesnym etapie, ale działa. Jest pod adresem:

https://github.com/luciotato/waitfor

Korzystając z wait.for , możesz wywołać dowolną standardową funkcję asynchroniczną nodejs, tak jakby to była funkcja synchronizacji.

używając wait. for your code może być:

var http=require('http');
var wait=require('wait.for');

http.createServer(function(req, res) {
  wait.launchFiber(handleRequest,req, res); //run in a Fiber, keep node spinning
}).listen(8080);


//in a fiber
function handleRequest(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  var someData = wait.for(getSomeDate,client);
  html += "<p>"+ someData +"</p>";
  var someOtherData = wait.for(getSomeOtherDate,client);
  html += "<p>"+ someOtherData +"</p>";
  var moreData = wait.for(getMoreData,client);
  html += "<p>"+ moreData +"</p>";
  res.write(html);
  res.end();
};

... lub jeśli chcesz być mniej rozwlekły (a także dodać wykrywanie błędów)

//in a fiber
function handleRequest(req, res) {
  try {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(
    "<h1>Demo page</h1>" 
    + "<p>"+ wait.for(getSomeDate,client) +"</p>"
    + "<p>"+ wait.for(getSomeOtherDate,client) +"</p>"
    + "<p>"+ wait.for(getMoreData,client) +"</p>"
    );
    res.end();
  }
  catch(err) {
   res.end('error '+e.message); 
  }

};

We wszystkich przypadkach getSomeDate , getSomeOtherDate i getMoreData powinny być standardowymi funkcjami asynchronicznymi z ostatnim parametrem wywołanie zwrotne funkcji (err, data)

jak w:

function getMoreData(client, callback){
  db.execute('select moredata from thedata where client_id=?',[client.id],
       ,function(err,data){
          if (err) callback(err);
          callback (null,data);
        });
}

1

Aby rozwiązać ten problem, napisałem nodent ( https://npmjs.org/package/nodent ), który niewidocznie wstępnie przetwarza twój JS. Twój przykładowy kod stałby się (tak naprawdę asynchroniczny - przeczytaj dokumentację).

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  someData <<= getSomeDate(client) ;

  html += "<p>"+ someData +"</p>";
  someOtherData <<= getSomeOtherDate(client) ;

  html += "<p>"+ someOtherData +"</p>";
  moreData <<= getMoreData(client) ;

  html += "<p>"+ moreData +"</p>";
  res.write(html);
  res.end();
});

Oczywiście istnieje wiele innych rozwiązań, ale przetwarzanie wstępne ma tę zaletę, że ma niewielkie lub żadne obciążenie w czasie wykonywania, a dzięki obsłudze map źródłowych można je również łatwo debugować.


0

Miałem ten sam problem. Widziałem główne biblioteki obsługujące funkcje asynchroniczne w węźle i prezentują one tak nienaturalne tworzenie łańcuchów (musisz użyć trzech lub więcej metod confs itp.), Aby zbudować swój kod.

Spędziłem kilka tygodni na opracowywaniu rozwiązania, które będzie proste i łatwe do odczytania. Proszę, spróbuj EnqJS . Wszystkie opinie będą mile widziane.

Zamiast:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  getSomeDate(client, function(someData) {
    html += "<p>"+ someData +"</p>";
    getSomeOtherDate(client, function(someOtherData) {
      html += "<p>"+ someOtherData +"</p>";
      getMoreData(client, function(moreData) {
        html += "<p>"+ moreData +"</p>";
        res.write(html);
        res.end();
      });
    });
  });

z EnqJS:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";

  enq(function(){
    var self=this;
    getSomeDate(client, function(someData){
      html += "<p>"+ someData +"</p>";
      self.return();
    })
  })(function(){
    var self=this;
    getSomeOtherDate(client, function(someOtherData){ 
      html += "<p>"+ someOtherData +"</p>";
      self.return();
    })
  })(function(){
    var self=this;
    getMoreData(client, function(moreData) {
      html += "<p>"+ moreData +"</p>";
      self.return();
      res.write(html);
      res.end();
    });
  });
});

Zwróć uwagę, że kod wydaje się być większy niż wcześniej. Ale nie jest zagnieżdżony jak wcześniej. Aby wyglądać bardziej naturalnie, łańcuchy są natychmiast nazywane:

enq(fn1)(fn2)(fn3)(fn4)(fn4)(...)

Aby powiedzieć, że wróciło, wewnątrz funkcji, którą wywołujemy:

this.return(response)

0

Robię to w dość prymitywny, ale skuteczny sposób. Np. Potrzebuję modelu z rodzicami i dziećmi i powiedzmy, że muszę zrobić dla nich oddzielne zapytania:

var getWithParents = function(id, next) {
  var getChildren = function(model, next) {
        /*... code ... */
        return next.pop()(model, next);
      },
      getParents = function(model, next) {
        /*... code ... */
        return next.pop()(model, next);
      }
      getModel = function(id, next) {
        /*... code ... */
        if (model) {
          // return next callbacl
          return next.pop()(model, next);
        } else {
          // return last callback
          return next.shift()(null, next);
        }
      }

  return getModel(id, [getParents, getChildren, next]);
}

0

Użyj włókien https://github.com/laverdet/node-fibers , dzięki czemu kod asynchroniczny wygląda jak synchroniczny (bez blokowania)

Osobiście używam tego małego opakowania http://alexeypetrushin.github.com/synchronize Próbka kodu z mojego projektu (każda metoda jest właściwie asynchroniczna, działa z plikiem asynchronicznym IO). Biblioteki pomocnicze async-control-flow.

_update: (version, changesBasePath, changes, oldSite) ->
  @log 'updating...'
  @_updateIndex version, changes
  @_updateFiles version, changesBasePath, changes
  @_updateFilesIndexes version, changes
  configChanged = @_updateConfig version, changes
  @_updateModules version, changes, oldSite, configChanged
  @_saveIndex version
  @log "updated to #{version} version"

0

Task.js oferuje Ci to:

spawn(function*() {
    try {
        var [foo, bar] = yield join(read("foo.json"),
                                    read("bar.json")).timeout(1000);
        render(foo);
        render(bar);
    } catch (e) {
        console.log("read failed: " + e);
    }
});

Zamiast tego:

var foo, bar;
var tid = setTimeout(function() { failure(new Error("timed out")) }, 1000);

var xhr1 = makeXHR("foo.json",
                   function(txt) { foo = txt; success() },
                   function(err) { failure() });
var xhr2 = makeXHR("bar.json",
                   function(txt) { bar = txt; success() },
                   function(e) { failure(e) });

function success() {
    if (typeof foo === "string" && typeof bar === "string") {
        cancelTimeout(tid);
        xhr1 = xhr2 = null;
        render(foo);
        render(bar);
    }
}

function failure(e) {
    xhr1 && xhr1.abort();
    xhr1 = null;
    xhr2 && xhr2.abort();
    xhr2 = null;
    console.log("read failed: " + e);
}

0

Po udzieleniu odpowiedzi przez innych stwierdziłeś, że Twoim problemem są zmienne lokalne. Wydaje się, że prostym sposobem na zrobienie tego jest napisanie jednej zewnętrznej funkcji, która będzie zawierała te zmienne lokalne, a następnie użycie zestawu nazwanych funkcji wewnętrznych i dostęp do nich po nazwie. W ten sposób zagnieżdżasz tylko dwie głębokie, niezależnie od tego, ile funkcji musisz połączyć w łańcuch.

Oto próba mojego początkującego wykorzystania mysqlmodułu Node.js z zagnieżdżeniem:

function with_connection(sql, bindings, cb) {
    pool.getConnection(function(err, conn) {
        if (err) {
            console.log("Error in with_connection (getConnection): " + JSON.stringify(err));
            cb(true);
            return;
        }
        conn.query(sql, bindings, function(err, results) {
            if (err) {
                console.log("Error in with_connection (query): " + JSON.stringify(err));
                cb(true);
                return;
            }
            console.log("with_connection results: " + JSON.stringify(results));
            cb(false, results);
        });
    });
}

Poniżej przedstawiono przepisanie przy użyciu nazwanych funkcji wewnętrznych. Funkcja zewnętrzna with_connectionmoże być również używana jako uchwyt na zmienne lokalne. (Tutaj, mam parametry sql, bindings, cbże działają w podobny sposób, ale można po prostu zdefiniować dodatkowe zmienne lokalne w with_connection.)

function with_connection(sql, bindings, cb) {

    function getConnectionCb(err, conn) {
        if (err) {
            console.log("Error in with_connection/getConnectionCb: " + JSON.stringify(err));
            cb(true);
            return;
        }
        conn.query(sql, bindings, queryCb);
    }

    function queryCb(err, results) {
        if (err) {
            console.log("Error in with_connection/queryCb: " + JSON.stringify(err));
            cb(true);
            return;
        }
        cb(false, results);
    }

    pool.getConnection(getConnectionCb);
}

Myślałem, że być może byłoby możliwe stworzenie obiektu ze zmiennymi instancji i użycie tych zmiennych instancji jako zamienników zmiennych lokalnych. Ale teraz stwierdzam, że powyższe podejście wykorzystujące funkcje zagnieżdżone i zmienne lokalne jest prostsze i łatwiejsze do zrozumienia. Oduczenie się OO zajmuje trochę czasu :-)

Oto moja poprzednia wersja ze zmiennymi obiektu i instancji.

function DbConnection(sql, bindings, cb) {
    this.sql = sql;
    this.bindings = bindings;
    this.cb = cb;
}
DbConnection.prototype.getConnection = function(err, conn) {
    var self = this;
    if (err) {
        console.log("Error in DbConnection.getConnection: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    conn.query(this.sql, this.bindings, function(err, results) { self.query(err, results); });
}
DbConnection.prototype.query = function(err, results) {
    var self = this;
    if (err) {
        console.log("Error in DbConnection.query: " + JSON.stringify(err));
        self.cb(true);
        return;
    }
    console.log("DbConnection results: " + JSON.stringify(results));
    self.cb(false, results);
}

function with_connection(sql, bindings, cb) {
    var dbc = new DbConnection(sql, bindings, cb);
    pool.getConnection(function (err, conn) { dbc.getConnection(err, conn); });
}

Okazuje się, że bindmożna to wykorzystać z pewną korzyścią. Pozwala mi pozbyć się nieco brzydkich anonimowych funkcji, które stworzyłem, które niewiele robiły, z wyjątkiem przekazywania samych siebie do wywołania metody. Nie mogłem przekazać metody bezpośrednio, ponieważ wiązałoby się to z niewłaściwą wartością this. Ale za pomocą bindmogę określić wartość this, której chcę.

function DbConnection(sql, bindings, cb) {
    this.sql = sql;
    this.bindings = bindings;
    this.cb = cb;
}
DbConnection.prototype.getConnection = function(err, conn) {
    var f = this.query.bind(this);
    if (err) {
        console.log("Error in DbConnection.getConnection: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    conn.query(this.sql, this.bindings, f);
}
DbConnection.prototype.query = function(err, results) {
    if (err) {
        console.log("Error in DbConnection.query: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    console.log("DbConnection results: " + JSON.stringify(results));
    this.cb(false, results);
}

// Get a connection from the pool, execute `sql` in it
// with the given `bindings`.  Invoke `cb(true)` on error,
// invoke `cb(false, results)` on success.  Here,
// `results` is an array of results from the query.
function with_connection(sql, bindings, cb) {
    var dbc = new DbConnection(sql, bindings, cb);
    var f = dbc.getConnection.bind(dbc);
    pool.getConnection(f);
}

Oczywiście nic z tego nie jest poprawnym JS z kodowaniem Node.js - spędziłem nad tym tylko kilka godzin. Ale może przy odrobinie dopracowania ta technika może pomóc?





0

Używając drutu, twój kod wyglądałby tak:

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});

    var l = new Wire();

    getSomeDate(client, l.branch('someData'));
    getSomeOtherDate(client, l.branch('someOtherData'));
    getMoreData(client, l.branch('moreData'));

    l.success(function(r) {
        res.write("<h1>Demo page</h1>"+
            "<p>"+ r['someData'] +"</p>"+
            "<p>"+ r['someOtherData'] +"</p>"+
            "<p>"+ r['moreData'] +"</p>");
        res.end();
    });
});

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.