Як уникнути тривалого вкладання асинхронних функцій у Node.js


158

Я хочу створити сторінку, яка відображає деякі дані з БД, тому я створив деякі функції, які отримують ці дані з моєї БД. Я просто новачок у Node.js, тому, наскільки я розумію, якщо я хочу використовувати їх на одній сторінці (відповідь HTTP), я повинен був би їх вкладати:

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();
      });
    });
  });

Якщо таких функцій багато, то вкладення стає проблемою .

Чи є спосіб цього уникнути? Я думаю, це стосується того, як ви поєднуєте кілька асинхронних функцій, що, здається, є чимось принциповим.


12
Отже, коли у вас є 10 функцій асинхронізації, у вас є 10 рівнів відступу?
Kay Pale

Це посилання може допомогти. stackoverflow.com/a/4631909/290340
Evan камбала

1
Ще одна проблема: вставлення іншої функції між getSomeDateі getSomeOtherDateзакінчується зміною відступу багатьох рядків, що робить історію git важче читати ( git blameнавіть марно після цього), і ви, ймовірно, робите помилки, роблячи це вручну
Daniel Alder

Відповіді:


73

Цікаве спостереження. Зауважте, що в JavaScript ви можете звичайно замінювати вбудовані функції анонімного зворотного виклику названими змінними функції.

Наступне:

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 ...
});

Можна переписати, щоб виглядати приблизно так:

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);

Однак, якщо ви не плануєте повторно використовувати логіку зворотного виклику в інших місцях, часто набагато простіше читати вбудовані анонімні функції, як у вашому прикладі. Це також позбавить вас від необхідності знайти ім'я для всіх зворотних дзвінків.

Крім того, зауважте, що, як зазначає @pst у коментарі нижче, якщо ви отримуєте доступ до змінних закриття у внутрішніх функціях, вищезгадане не буде прямим перекладом. У таких випадках використання вбудованих анонімних функцій ще більш переважно.


26
Однак (і справді просто для розуміння компромісу), коли він не вкладений, деяка семантика закриття змінних може бути втрачена, так що це не прямий переклад. У наведеному вище прикладі доступ до 'res' in getMoreDataвтрачається.

2
Я думаю, що ваше рішення порушено: someDataParserчи насправді аналізує ВСІ дані, оскільки він також викликає getMoreData. У цьому сенсі назва функції невірна, і стає очевидним, що ми фактично не усунули проблему введення.
Костянтин Шуберт

63

Кей, просто використовуй один із цих модулів.

Це перетвориться так:

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();
            });
        });
    });
});

У це:

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
Швидкий огляд потоку js, кроку та асинхронізації, і, здається, вони мають справу лише з порядком виконання функції. У моєму випадку є доступ до змінних вбудованого закриття в кожному відступі. Так, наприклад, функції працюють так: отримати HTTP req / res, отримати userid від БД для cookie, отримати електронну пошту для пізнішого userid, отримати більше даних для пізнішої електронної пошти, ..., отримати X для подальшого Y, ... Якщо я не помиляюся, ці рамки лише запевняють, що функції асинхронізації будуть виконуватись у правильному порядку, але в кожному функціональному тілі немає способу отримати змінну, яку, природно, забезпечує закриття (?) Спасибі :)
Кей Пейл

9
Щодо ранжування цих бібліотек, я перевірив кількість "зірок" на кожній у Github. async має найбільше - близько 3000, крок - близько 1000, інші - значно менше. Звичайно, у них все не те саме :-)
kgilpin

3
@KayPale Я, як правило, використовую async.waterfall, і інколи матиму власні функції для кожного етапу / кроку, які будуть проходити по тому, що потрібно наступний крок, або визначати змінні перед викликом async.METHOD, щоб він був доступний вниз. Також використовуватиме METHODNAME.bind (...) для моїх викликів async. *, Що теж добре працює.
Tracker1

Швидке запитання: Чи є у вашому списку модулів два останніх однакові? Тобто "async.js" і "async"
dari0h

18

Здебільшого я погодився б із Даніелем Вассалло. Якщо ви можете розбити складну і глибоко вкладену функцію на окремі іменовані функції, то це, як правило, хороша ідея. У часи, коли є сенс робити це всередині однієї функції, ви можете використовувати одну з безлічі наявних бібліотек асинхронних node.js. Люди придумали безліч різних способів вирішити це питання, тому подивіться на сторінку модулів node.js і подивіться, що ви думаєте.

Я сам написав для цього модуль під назвою async.js . Використовуючи це, вищевказаний приклад можна оновити до:

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();
  });
});

Одна приємна річ у цьому підході полягає в тому, що ви можете швидко змінити свій код, щоб отримати дані паралельно, змінивши функцію "серія" на "паралельну". Більше того, async.js також працюватиме всередині браузера, тому ви можете використовувати ті самі методи, що і в node.js, якщо ви зіткнетесь із будь-яким хитрим кодом async.

Сподіваюся, що це корисно!


Привіт Каолан і дякую за відповідь! У моєму випадку є доступ до змінних вбудованого закриття в кожному відступі. Так, наприклад, функції працюють так: отримати HTTP req / res, отримати userid від БД для cookie, отримати електронну пошту для пізнішого userid, отримати більше даних для пізнішої електронної пошти, ..., отримати X для подальшого Y, ... Якщо я не помиляюся, запропонований вами код гарантує лише те, що функції асинхронізації виконуватимуться у правильному порядку, але в кожному органі функції немає способу отримати змінну, яку, природно, забезпечує закриття в моєму вихідному коді. Це так?
Кей Блед

3
Те, що ви намагаєтеся досягти, архітектурно називається «трубопровід даних». Для таких випадків можна використовувати водоспад асинхрон.
Рудольф Мейрінг

18

Ви можете використовувати цей трюк із масивом, а не вкладеними функціями чи модулем.

Куди простіше на очах.

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()();

Ви можете розширити ідіому для паралельних процесів або навіть паралельних ланцюгів процесів:

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

Мені дуже подобається async.js для цієї мети.

Питання вирішується командою водоспаду:

водоспад (завдання, [зворотний дзвінок])

Запускає масив функцій послідовно, кожен передаючи свої результати наступному в масиві. Однак якщо будь-яка з функцій передає помилку до зворотного дзвінка, наступна функція не виконується і основний зворотний виклик негайно викликається з помилкою.

Аргументи

завдання - Масив функцій, які потрібно виконати, кожній функції передається зворотний виклик (помилка, результат1, результат2, ...), яку вона повинна викликати після завершення. Перший аргумент - це помилка (яка може бути нульовою), і будь-які подальші аргументи будуть передані як аргументи для наступного завдання. зворотний виклик (помилка, [результати]) - необов'язковий зворотний виклик, який потрібно запустити після завершення всіх функцій. Це буде передано результати зворотного виклику останнього завдання.

Приклад

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'    
});

Що стосується змінних req, res, вони поділяться в тій самій області, що і функція (req, res) {}, яка уклала весь виклик async.waterfall.

Мало того, що асинхронізація дуже чиста. Що я маю на увазі, що я змінюю безліч справ, таких як:

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

Спочатку:

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

Тоді до цього:

function2(o,cb);

Тоді до цього:

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

Це також дозволяє дуже швидко заздалегідь викликати багато функцій, підготовлених до асинхронізації, з програми util.js. Просто зав'яжіть, що ви хочете зробити, переконайтеся, що o, cb є універсальним. Це значно пришвидшує весь процес кодування.


11

Вам потрібно трохи синтаксичного цукру. Перевірте це:

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();
}); 

Досить акуратно , чи не так? Ви можете помітити, що html став масивом. Це частково тому, що рядки незмінні, тому вам краще скористатися буферизацією результату в масиві, ніж відкидати більші та більші рядки. Інша причина - через ще один приємний синтаксис с bind.

Queueу прикладі насправді лише приклад і поряд з цим partialможна реалізувати наступним чином

// 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 () буде просто виконувати партії один за одним, не чекаючи результатів викликів async.
ngn

Місце на, дякую. Я оновив відповідь. Ось тест: jsbin.com/ebobo5/edit (з додатковою lastфункцією)
gblazex

Привіт галамбалаз і дякую за відповідь! У моєму випадку є доступ до змінних вбудованого закриття в кожному відступі. Так, наприклад, функції працюють так: отримати HTTP req / res, отримати userid від БД для cookie, отримати електронну пошту для пізнішого userid, отримати більше даних для пізнішої електронної пошти, ..., отримати X для подальшого Y, ... Якщо я не помиляюся, запропонований вами код гарантує лише те, що функції асинхронізації виконуватимуться у правильному порядку, але в кожному органі функції немає способу отримати змінну, яку, природно, забезпечує закриття в моєму вихідному коді. Це так?
Кей Блед

1
Добре, що ви напевно втрачаєте закриття у всіх відповідях. Що ви можете зробити, це створити об’єкт у глобальній області для спільних даних. Так, наприклад, ваша перша функція додає, obj.emailа ваша наступна функція використовує, obj.emailа потім видаляє її (або просто призначає null).
gblazex

7

Я закоханий у Async.js з тих пір, як я його знайшов. Він має async.seriesфункцію, яку ви можете використовувати, щоб уникнути тривалого гніздування.

Документація: -


серія (завдання, [зворотний дзвінок])

Запустити масив функцій послідовно, кожна з яких виконується, коли попередня функція виконана. [...]

Аргументи

tasks- Масив функцій, які потрібно запустити, кожній функції передається зворотний виклик, вона повинна викликати після завершення. callback(err, [results])- Необов'язковий зворотний дзвінок, який потрібно запустити після завершення всіх функцій. Ця функція отримує масив усіх аргументів, переданих зворотним викликам, що використовується в масиві.


Ось як ми можемо застосувати його до вашого прикладу коду: -

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

Найпростіший синтаксичний цукор, який я бачив, - це багатообіцяючий вузол.

npm встановити вузол-обіцянка || git clone https://github.com/kriszyp/node-promise

Використовуючи це, ви можете пов’язати методи асинхронізації як:

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

Повернене значення кожного доступне як аргумент у наступному.


3

Що ви там зробили, це взяти шаблон асинхронізації та застосувати його до 3-х функцій, що викликаються послідовно, кожна з яких очікує завершення попередньої перед запуском - тобто ви зробили їх синхронними . Справа в асинхронному програмуванні полягає в тому, що ви можете мати кілька функцій, які працюють відразу, і не потрібно чекати, коли кожна програма завершиться.

якщо getSomeDate () не забезпечує нічого getSomeOtherDate (), що нічого не дає getMoreData (), то чому б ви не називали їх асинхронно, як дозволяє js, або якщо вони взаємозалежні (а не асинхронні), запишіть їх як одна функція?

Вам не потрібно використовувати вкладку для управління потоком - наприклад, змушуйте кожну функцію закінчити, викликаючи загальну функцію, яка визначає, коли всі 3 завершено, а потім надсилає відповідь.


2

Припустимо, ви могли це зробити:

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();
        }
    ]);
});

Вам потрібно лише реалізувати ланцюжок (), щоб вона частково застосувала кожну функцію до наступної, і негайно викликала лише першу функцію:

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

Привіт ngn і дякую за відповідь! У моєму випадку є доступ до змінних вбудованого закриття в кожному відступі. Так, наприклад, функції працюють так: отримати HTTP req / res, отримати userid від БД для cookie, отримати електронну пошту для пізнішого userid, отримати більше даних для пізнішої електронної пошти, ..., отримати X для подальшого Y, ... Якщо я не помиляюся, запропонований вами код гарантує лише те, що функції асинхронізації виконуватимуться у правильному порядку, але в кожному органі функції немає способу отримати змінну, яку, природно, забезпечує закриття в моєму вихідному коді. Це так?
Кей Блед

2

Пекло зворотного дзвінка легко уникнути в чистому javascript із закриттям. Рішення нижче передбачає, що всі зворотні дзвінки слідують за функцією (помилка, дані) підпису.

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

Нещодавно я створив більш просту абстракцію під назвою wait.for для виклику функцій асинхронізації в режимі синхронізації (на основі Fibers). Це на ранній стадії, але працює. Це за адресою:

https://github.com/luciotato/waitfor

Використовуючи wait.for , ви можете викликати будь-яку стандартну функцію асинхронізації nodejs, як ніби це функція синхронізації.

за допомогою wait.for вашого коду може бути:

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();
};

... або якщо ви хочете бути менш докладно (а також додати помилку вловлювання)

//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); 
  }

};

У всіх випадках getSomeDate , getSomeOtherDate та getMoreData повинні бути стандартними функціями асинхронізації з останнім параметром функцією зворотного виклику функції (помилка, дані)

а саме:

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

Для вирішення цієї проблеми я написав nodent ( https://npmjs.org/package/nodent ), який невидимо попередньо обробляє ваш JS. Ваш прикладний код стане (асинхронний, насправді - читайте документи).

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();
});

Зрозуміло, що існує багато інших рішень, але перед попередньою обробкою є перевага в тому, що вони мають невеликі витрати часу на експлуатацію або відсутність, а завдяки підтримці вихідної карти також легко виправити помилку.


0

У мене була така ж проблема. Я бачив основні лібрики, щоб виконувати функції асинхронізації, і вони представляють настільки неприродні ланцюжки (для створення коду потрібно використовувати три або більше методів confs тощо).

Я витратив кілька тижнів на розробку рішення, яке було б простим і легким для читання. Спробуйте, спробуйте EnqJS . Всі думки будуть вдячні.

Замість:

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();
      });
    });
  });

за допомогою 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();
    });
  });
});

Зауважте, що код здається більшим, ніж раніше. Але це не вкладено, як раніше. Щоб виглядати більш природним, ланцюги називаються негайно:

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

І сказати, що він повернувся, всередині функції, яку ми називаємо:

this.return(response)

0

Я роблю це досить примітивно, але ефективно. Наприклад, мені потрібно отримати модель з її батьками та дітьми, і скажімо, що мені потрібно робити окремі запити для них:

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

Використовуйте волокна https://github.com/laverdet/node-fibers, це робить асинхронний код схожим на синхронний (без блокування)

Я особисто використовую цю маленьку обгортку http://alexeypetrushin.github.com/synchronize Зразок коду з мого проекту (кожен метод насправді асинхронний, що працює з IO файлу async), я навіть боюся уявити, який би це було безлад з зворотним викликом або бібліотеки помічників 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 пропонує вам це:

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);
    }
});

Замість цього:

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

Після того, як інші відповіли, ви заявили, що вашою проблемою були локальні змінні. Зрозуміло, що це простий спосіб - це написати одну зовнішню функцію, яка містить ці локальні змінні, а потім використовувати купу названих внутрішніх функцій та отримати доступ до них по імені. Таким чином, ви завжди гніздите лише дві глибокі, незалежно від того, скільки функцій вам потрібно зв'язати.

Ось моя спроба новачка використовувати mysqlмодуль Node.js з вкладкою:

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);
        });
    });
}

Далі йде переписування, використовуючи названі внутрішні функції. Зовнішня функція також with_connectionможе використовуватися як держатель для локальних змінних. (Ось, у мене є параметри sql, bindings, cbякі діють подібним чином, але ви можете просто визначити деякі додаткові локальні змінні 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);
}

Я думав, що, можливо, можна буде створити об'єкт зі змінними екземпляра та використовувати ці змінні екземпляри як заміну локальних змінних. Але тепер я вважаю, що вищезазначений підхід із використанням вкладених функцій та локальних змінних простіший і легший для розуміння. Треба трохи часу, щоб навчитися ОО, здається :-)

Отже ось моя попередня версія зі змінними об'єкта та екземпляра.

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); });
}

Виявляється, bindможна скористатися якоюсь перевагою. Це дозволяє мені позбутися дещо потворних анонімних функцій, створених мною, які нічого не зробили, окрім як направити себе на виклик методу. Я не міг передати метод безпосередньо, оскільки він був би пов'язаний з неправильним значенням this. Але з bind, я можу вказати значення того, thisщо я хочу.

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);
}

Звичайно, нічого з цього не є належним JS з кодуванням Node.js - я просто витратив на це пару годин. Але, може, з невеликим поліруванням ця техніка може допомогти?



0

Якщо ви не хочете використовувати "крок" або "послідовність", спробуйте "рядок", яка є простою функцією для зменшення вкладеного зворотного зв'язку з асинхронністю.

https://github.com/kevin0571/node-line



0

За допомогою дроту ваш код виглядатиме так:

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();
    });
});

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.