是否可以编写异步Node.js代码“清洁”?

时间:2014-10-18 23:22:19

标签: node.js asynchronous

在Node.js中编码时,我遇到很多情况,因为很难实现与数据库查询(I / O)混合的精细逻辑。

考虑一个用python编写的例子。我们需要遍历一个值数组,对于我们查询数据库的每个值,然后,根据结果,我们需要计算平均值。

def foo:
  a = [1, 2, 3, 4, 5]
  result = 0
  for i in a:
    record = find_from_db(i) # I/O operation
    if not record:
      raise Error('No record exist for %d' % i)
    result += record.value

  return result / len(a)

Node.js中的相同任务

function foo(callback) {
  var a = [1, 2, 3, 4, 5];
  var result = 0;
  var itemProcessed = 0;
  var error;
  function final() {
    if (itemProcessed == a.length) {
      if (error) {
        callback(error);
      } else {
        callback(null, result / a.length);
      }
    }
  }
  a.forEach(function(i) {
    // I/O operation
    findFromDb(function(err, record) {
      itemProcessed++;
      if (err) {
        error = err;
      } else if (!record) {
        error = 'No record exist for ' + i;
      } else {
        result += record.value;
      }
      final();
    });
  });
}

你可以看到这样的代码更难写/读,而且更容易出错。 我的问题:

  1. 有没有办法让上面的Node.js代码更清洁?
  2. 想象一下更复杂的逻辑。例如,当我们从db获取记录时,我们可能需要根据某些条件执行另一个db查询。在Node.js中成为一场噩梦。处理此类任务的常见模式是什么?
  3. 根据您的经验,使用Node.js进行编码时,性能提升是否值得降低工作效率?
  4. 是否有其他更易于使用的异步I / O框架/语言?

6 个答案:

答案 0 :(得分:5)

回答你的问题:

  1. async等库可以在处理异步任务时为常见场景提供各种解决方案。对于“回调地狱”问题,有很多方法可以避免这种情况,包括(但不限于)命名功能并将其拉出来,模块化代码和使用承诺。

  2. 或多或少你现在拥有的是一个相当常见的模式:拥有一个带有要调用的函数数组的计数器和函数索引变量。同样,async可以在这里提供帮助,因为它可以减少您可能会经常重复的这种样板。 async目前没有真正允许跳过单个任务的方法,但是如果你正在编写样板文件,你可以自己轻松地做到这一点(例如,只需将函数索引变量增加2)。

    < / LI>
  3. 根据我自己的经验,如果你正确设计你的javascript代码并考虑异步并使用很多工具如async,你会发现用节点开发更容易。在节点中编写异步与同步通常总是会更复杂(尽管与回调/承诺相比,生成器,光纤等更少)。

  4. 我个人认为根据单一方面决定一种语言是不值得的。您必须考虑的不仅仅是语言的设计,例如社区的大小,第三方库的可用性,性能,技术支持选项,代码调试的简易性等。

答案 1 :(得分:4)

只需更紧凑地编写代码:

// parallel version
function foo (cb) {
  var items = [ 1, 2, 3, 4, 5 ];
  var pending = items.length;
  var result = 0;

  items.forEach(function (item) {
    findFromDb(item, function (err, record) {
      if (err) return cb(err);
      if (!record) return cb(new Error('No record for: ' + item))
      result += record.value / items.length;
      if (-- pending === 0) cb(null, result);
    });
  });
}

与你发布的python的9个sloc相比,它在13个源代码行中显示。但是,与您发布的python不同,此代码并行运行所有作业。

为了在系列中做同样的事情,我通常做的一个技巧是内联定义的next()函数调用自身并从数组中弹出一个作业:

// sequential version
function foo (cb) {
  var items = [ 1, 2, 3, 4, 5 ];
  var len = items.length;
  var result = 0;

  (function next () {
    if (items.length === 0) return cb(null, result);
    var item = items.shift();

    findFromDb(item, function (err, record) {
      if (err) return cb(err);
      if (!record) return cb(new Error('No record for: ' + item))
      result += record.value / len;
      next();
    });
  })();
}

这次,15行。好消息是,您可以轻松控制动作是并行还是顺序发生,还是介于两者之间。在像python这样的语言中,这并不是那么容易,因为一切都是同步的,你必须做大量的工作,比如线程或事件库,以使事情恢复到异步。尝试在python中实现你所拥有的并行版本!它肯定会比节点版本更长。

对于promise / async路由:对于这些相对简单的任务,使用普通函数实际上并不是那么难或坏。在将来(或在带有--harmony的节点0.11 +中),您可以使用生成器和类似co的库,但该功能尚未广泛部署。

答案 2 :(得分:3)

这里的每个人似乎都在建议async,这是一个很棒的图书馆。但是要给出另一个建议,你应该看一下Promises ,这是一种新的内置语言(目前有几种非常好的polyfill)。它允许您以看起来更结构化的方式编写异步代码。例如,看看这段代码:

var items = [ 1, 2, 3, 4 ];
var processItem = function(item, callback) {
    // do something async ...
};
var values = [ ];
items.forEach(function(item) {
    processItem(item, function(err, value) {
        if (err) {
            // something went wrong
        }
        values.push(value);
        // all of the items have been processed, move on
        if (values.length === items.length) {
            doSomethingWithValues(values, function(err) {
                if (err) {
                    // something went wrong
                }
                // and we're done
            });
        }
    });
});
function doSomethingWithValues(values, callback) {
    // do something async ...
}

使用promises,会写成这样的东西:

var items = [ 1, 2, 3, 4 ];
var processItem = function(item) {
    return new Promise(function(resolve, reject) {
        // do something async ...
    });
};
var doSomethingWithValues = function(values) {
    return new Promise(function(resolve, reject) {
        // do something async ...
    });
};
// promise.all returns a new promise that will resolve when all of the promises passed to it have resolved
Promise.all(items.map(processItem))
    .then(doSomethingWithValues)
    .then(function() {
        // and we're done
    })
    .catch(function(err) {
        // something went wrong
    });

第二个版本更清洁,更简单,甚至几乎没有表面承诺真正的力量。并且,正如我所说,Promise作为内置的新语言在es6中,所以(最终)你甚至不需要加载到库中,它只是可用。

答案 3 :(得分:2)

  1. 不要使用匿名(未命名)函数,它们会使代码变得难看,并且它们会使调试变得更加困难,因此请始终为函数命名并在函数范围之外定义它们而不是内联。
  2. 这是Node.js的一个真正问题(它被称为回调地狱或厄运的金字塔,..)你可以通过使用promises或使用async.js来解决这个问题,它有很多函数来处理不同的情况(瀑布,平行,系列,汽车,...)
  3. 好的,性能提升绝对是件好事,并没有那么大的损失(当你开始掌握它时),Node.js社区也很棒。
  4. 检查async.jsq

答案 4 :(得分:1)

我使用async的次数越多,我就越喜欢它,我更喜欢节点。让我举一个关于服务器初始化的简单示例。

async.parallel ({
    "job1": loadFromCollection1,
    "job2": loadFromCollection2,
},
function (initError, results) {
    if (initError) {
        console.log ("[INIT] Server initialization error occurred: " + JSON.stringify(initError, null, 3));
        return callback (initError);
    }
    // Do more stuff with the results
});

实际上,可以遵循这种相同的方法,并且可以将不同的参数传递给与各种作业相对应的不同的函数;例如,见Passing arguments to async.parallel in node.js

对你说实话,我更喜欢节点方式,它也是非阻塞的。我认为node强迫某人拥有更好的设计,有时您会花时间创建更多定义并将数组中的函数和对象分组,以便您可以编写更好的代码。我认为最终你想要利用async的一些变体并相应地混合和合并内容。在我看来,当你考虑到节点是异步时,花一些额外的时间和思考代码也是值得的。

除此之外,我认为这是一种习惯。为节点编写代码的人越多,就越能改进并编写更好的异步代码。在节点上有什么好处是它真的迫使某人编写更健壮的代码,因为人们开始更多地关注来自所有函数的所有错误代码。例如,人们检查的频率,比如mallocnew是否成功,并且在命令发出后没有NULL指针的错误处理程序?编写异步代码虽然强制一个人尊重事件和事件所具有的错误代码。我想一个明显的原因是,一个人尊重一个人编写的代码,最后我们必须编写返回错误的代码,以便调用者知道发生了什么。

我真的认为你需要给它更多时间并开始使用异步更多。这就是全部。

答案 5 :(得分:0)

&#34;如果您尝试使用纯node.js对bussiness db login进行编码,则直接回调地狱&#34;

我最近创建了一个名为WaitFor的简单抽象,以同步模式调用异步函数(基于Fibers):https://github.com/luciotato/waitfor

检查数据库示例:

数据库示例(伪代码)

pure node.js(温和的回调地狱):

var db = require("some-db-abstraction");

function handleWithdrawal(req,res){  
    try {
        var amount=req.param("amount");
        db.select("* from sessions where session_id=?",req.param("session_id"),function(err,sessiondata) {
            if (err) throw err;
            db.select("* from accounts where user_id=?",sessiondata.user_ID),function(err,accountdata) {
                if (err) throw err;
                    if (accountdata.balance < amount) throw new Error('insufficient funds');
                    db.execute("withdrawal(?,?),accountdata.ID,req.param("amount"), function(err,data) {
                        if (err) throw err;
                        res.write("withdrawal OK, amount: "+ req.param("amount"));
                        db.select("balance from accounts where account_id=?", accountdata.ID,function(err,balance) {
                            if (err) throw err;
                            res.end("your current balance is "  + balance.amount);
                        });
                    });
                });
            });
        }
        catch(err) {
            res.end("Withdrawal error: "  + err.message);
    }  

注意:上面的代码,虽然它看起来会捕获异常,它不会。 用回调地狱捕捉异常会增加很多痛苦,而且我不确定你是否会有这样的反应。参数 回应用户。如果有人喜欢修复这个例子......请成为我的客人。

使用 wait.for

var db = require("some-db-abstraction"), wait=require('wait.for');

function handleWithdrawal(req,res){  
    try {
        var amount=req.param("amount");
        sessiondata = wait.forMethod(db,"select","* from session where session_id=?",req.param("session_id"));
        accountdata= wait.forMethod(db,"select","* from accounts where user_id=?",sessiondata.user_ID);
        if (accountdata.balance < amount) throw new Error('insufficient funds');
        wait.forMethod(db,"execute","withdrawal(?,?)",accountdata.ID,req.param("amount"));
        res.write("withdrawal OK, amount: "+ req.param("amount"));
        balance=wait.forMethod(db,"select","balance from accounts where account_id=?", accountdata.ID);
        res.end("your current balance is "  + balance.amount);
        }
    catch(err) {
        res.end("Withdrawal error: "  + err.message);
}  

注意:将按预期捕获异常。 将使用this = db

调用db方法(db.select,db.execute)

您的代码

为了使用wait.for,您必须标准化您的CALLBACKS才能运行(错误,数据)

如果您标准化您的回复,您的代码可能如下所示:

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

//run in a Fiber
function process() {
  var a = [1, 2, 3, 4, 5];
  var result = 0;
  a.forEach(function(i) {
    // I/O operation
    var record = wait.for(findFromDb,i); //call & wait for async function findFromDb(i,callback)
    if (!record) throw new Error('No record exist for ' + i);
    result += record.value;
  });

  return result/a.length;
}

function inAFiber(){
   console.log('result is: ',process());
}

// run the loop in a Fiber (keep node spinning)
wait.launchFiber(inAFiber);

见?离python很近,没有回调地狱