Как избежать длительного вложения асинхронных функций в Node.js - PullRequest
156 голосов
/ 20 ноября 2010

Я хочу создать страницу, которая отображает некоторые данные из БД, поэтому я создал несколько функций, которые получают эти данные из моей БД.Я просто новичок в Node.js, так что, насколько я понимаю, если я захочу использовать их все на одной странице (HTTP-ответ), мне придется их всех вкладывать:1003 * Если таких функций много, то вложение становится проблемой .

Есть ли способ избежать этого?Я предполагаю, что это связано с тем, как вы комбинируете несколько асинхронных функций, что кажется фундаментальным.

Ответы [ 23 ]

2 голосов
/ 22 ноября 2010

Предположим, вы можете сделать это:

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

Вам нужно только реализовать chain (), чтобы он частично применял каждую функцию к следующей и сразу вызывал только первую функцию:

function chain(fs) {
    var f = function () {};
    for (var i = fs.length - 1; i >= 0; i--) {
        f = fs[i].partial(f);
    }
    f();
}
1 голос
/ 05 января 2014

Чтобы решить эту проблему, я написал 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();
});

ПонятноСуществует множество других решений, но преимущество предварительной обработки заключается в том, что она не требует больших затрат времени выполнения или вообще не требует никаких затрат, а благодаря поддержке карты источника также легко отлаживается.

1 голос
/ 22 августа 2013

Я недавно создал более простую абстракцию под названием wait.for для вызова асинхронных функций в режиме синхронизации (на основе волокон).Это на ранней стадии, но работает.Он находится по адресу:

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 должны быть стандартными асинхронными функциями с последним параметром обратного вызова функции (err,данные)

как в:

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);
        });
}
0 голосов
/ 19 октября 2015

Используя провод ваш код будет выглядеть так:

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();
    });
});
0 голосов
/ 03 марта 2015

C # -подобная асинхронность - это еще один способ сделать это

https://github.com/yortus/asyncawait

async(function(){

    var foo = await(bar());
    var foo2 = await(bar2());
    var foo3 = await(bar2());

}
0 голосов
/ 27 января 2015

Если вы не хотите использовать «step» или «seq», попробуйте «line», которая является простой функцией для уменьшения вложенного асинхронного обратного вызова.

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

0 голосов
/ 28 февраля 2014

async.js хорошо работает для этого. Я наткнулся на эту очень полезную статью, которая объясняет необходимость и использование async.js с примерами: http://www.sebastianseilund.com/nodejs-async-in-practice

0 голосов
/ 09 октября 2011

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

Я потратил несколько недель на разработкуРешение должно быть простым и легким для чтения.Пожалуйста, попробуйте 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 голосов
/ 12 октября 2013

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

Вот попытка моего новичка использовать модуль 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);
}
* 1014Я думал, что, возможно, можно будет создать объект с переменными экземпляра и использовать эти переменные экземпляра в качестве замены локальных переменных.Но теперь я обнаружил, что описанный выше подход с использованием вложенных функций и локальных переменных проще и понятнее.Кажется, требуется некоторое время, чтобы отключить OO: -)

Итак, вот моя предыдущая версия с переменными объекта и экземпляра.

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 голосов
/ 15 июля 2012

Я делаю это довольно примитивно, но эффективно.Например, мне нужно получить модель с ее родителями и детьми, и скажем, мне нужно сделать для них отдельные запросы:

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]);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...