在nodejs中callback地狱?

在下面的代码中,我在callback? 如何克服这种情况,而不使用纯JavaScript中的任何asynchronous模块?

emailCallBack(e_data, email); if (email_list.length) { checkEmail(email_list.pop()); } else { completionCallback(); } 

上面的代码被复制到多个位置,以使代码正常工作。

 function processInviteEmails(email_list, user_id, emailCallBack, completionCallback){ function checkEmail(email){ try { check(email).isEmail(); //is valid email checkConnected(email, user_id, function(connect_status, user_row, user_meta_row, connect_row){ var e_data; //insert to connect and send msg to queue if(connect_status === 'not connected'){ var cur_date = moment().format('YYYY-MM-DD'); var dbData = { "first_name": '', "last_name": '', "email": email, "user_id": user_id, "status": "invited", "unsubscribe_token": crypto.randomBytes(6).toString('base64'), "created": cur_date, "modified": cur_date }; ConnectModel.insert(dbData, function(result){ if (result.insertId > 0) { //send to email queue //Queue Email MailTemplateModel.getTemplateData('invitation', function(res_data){ if(res_data.status === 'success'){ var unsubscribe_hash = crypto.createHash("md5") .update(dbData.unsubscribe_token + email) .digest('hex'); var unsubscribe_link = app.locals.SITE_URL+'/unsubscribe/' + result.insertId + '/' + unsubscribe_hash; var template_row = res_data.template_row; var user_full_name = user_row.user_firstname+' '+ user_row.user_lastname; var invitation_link = 'http://'+user_row.url_alias+'.'+ app.locals.SITE_DOMAIN; var mailOptions = { "type": 'invitation', "to": dbData.email, "from_name" : user_full_name, "subject": template_row.message_subject .replace('[[USER]]', user_full_name), "text": template_row.message_text_body .replace('[[USER]]', user_full_name) .replace('[[INVITATION_LINK]]', invitation_link) .replace('[[UNSUBSCRIBE_LINK]]', unsubscribe_link), "html": template_row.message_body .replace('[[USER]]', user_full_name) .replace('[[INVITATION_LINK]]', invitation_link) .replace('[[UNSUBSCRIBE_LINK]]', unsubscribe_link) }; mailOptions = JSON.stringify(mailOptions); //send email to queue sqsHelper.addToQueue(cfg.sqs_invitation_url, mailOptions, function(data){ if(data){ e_data = null; } else{ e_data = new Error('Unable to Queue '); } emailCallBack(e_data, email); if (email_list.length) { checkEmail(email_list.pop()); } else { completionCallback(); } }); } else{ e_data = new Error('Unable to get email template'); emailCallBack(e_data, email); if (email_list.length) { checkEmail(email_list.pop()); } else { completionCallback(); } } }); } else{ e_data = new Error('Unable to Insert connect'); emailCallBack(e_data, email); if (email_list.length) { checkEmail(email_list.pop()); } else { completionCallback(); } } }); } else{ e_data = new Error('Already connected'); emailCallBack(e_data, email); if (email_list.length) { checkEmail(email_list.pop()); } else { completionCallback(); } } }); } catch (e) { //invalid email emailCallBack(e, email); if (email_list.length) { checkEmail(email_list.pop()); } else { completionCallback(); } } } checkEmail(email_list.pop()); } 

是的,你在回拨地狱。 假设你不想使用asynchronous的解决scheme(我怀疑你可以certificate除了偏见之外)包括:

1 )做更多的顶级function。 根据经验,每个函数都应该执行1或2个IO操作。

2 )调用这些函数,让你的代码遵循一个短小的核心函数列表的模式,通过一小串控制stream“粘合”函数组织成业务逻辑。

代替:

 saveDb1 //lots of code saveDb2 //lots of code sendEmail //lots of code 

目的:

 function saveDb1(arg1, arg2, callback) {//top-level code} function saveDb2(arg1, arg2, callback) {//top-level code} function sendEmail(arg1, arg2, callback) {//top-level code} function businessLogic(){//uses the above to get the work done} 

3 )使用更多的函数参数,而不是太依赖闭包

4 )发射事件和解除您的代码! 看看你如何嵌套代码写入数据库的东西,然后build立一个电子邮件,并将其添加到队列? 难道你不知道这两个人怎么不需要一个人存在吗? 电子邮件很适合发送核心业务逻辑的事件和一个收听这些事件和排队邮件的电子邮件模块。

5 )将应用程序级别的服务连接代码从特定的事务业务逻辑中解耦。 应该更广泛地处理与networking服务的连接,而不是embedded一组特定的业务逻辑。

6 )阅读其他模块的例子

至于你应该使用一个asynchronous库,你可以而且应该做出你自己的想法,但在你知道,并且很好地知道这些方法的每一个之后:

  1. callback和基本的function性的JavaScript技术
  2. 事件
  3. 许诺
  4. 帮助程序库(asynchronous,步骤,敏捷等)

任何严重的node.js开发人员都知道如何在这些范例中使用和工作。 是的,每个人都有他们喜欢的方法,也许有些书呆子对非优惠的方法感到愤怒,但是这些方法都不是很难,而且不能指出你从头开始编写的一些不重要的代码,每个范例。 此外,你应该尝试几个助手库,了解他们如何工作,为什么他们要为您节省样板。 学习蒂姆·卡斯韦尔的Step或者凯伦·麦克马洪的async将会非常有启发性。 你看过everyauth源代码对promises的使用吗? 我个人不喜欢它,但是我当然不得不承认,作者已经把这个重复出现在这个图书馆的每一个重要部分都榨干了,他用承诺的方式会把你的大脑变成椒盐脆饼。 这些人是有很多教训的巫师。 不要嘲笑这些图书馆只是为了赶时髦点或任何其他的。

callbackhell.com也是一个很好的外部资源。

“如果你尝试使用纯粹的node.js编写商业数据库login,你直接callback地狱”

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

检查数据库的例子:

数据库示例(伪代码)

纯粹的node.js(温和的callback地狱):

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

注意:上面的代码,虽然看起来会捕获exception, 但不会 。 捕捉exception与callback地狱增加了很多的痛苦,我不知道你是否有'RES'参数来响应用户。 如果有人想修正这个例子…做我的客人。

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

注意:例外情况将如预期那样捕捉。 db方法(db.select,db.execute)将用this = db来调用

你的代码

为了使用wait.for,你必须标准化你的callback函数(err,data)

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

 //run in a Fiber function processInviteEmails(email_list, user_id, emailCallBack, completionCallback){ while (email_list.length) { var email = email_list.pop(); try { check(email).isEmail(); //is valid email or throw var connected_data = wait.for(checkConnected,email,user_id); if(connected_data.connect_status !== 'not connected') throw new Error('Already connected'); //insert to connect and send msg to queue var cur_date = moment().format('YYYY-MM-DD'); var dbData = { "first_name": '', "last_name": '', "email": email, "user_id": user_id, "status": "invited", "unsubscribe_token": crypto.randomBytes(6).toString('base64'), "created": cur_date, "modified": cur_date }; result = wait.forMethod(ConnectModel,'insert',dbData); // ConnectModel.insert shuold have a fn(err,data) as callback, and return something in err if (data.insertId <= 0) //send to email queue //Queue Email res_data = wait.forMethod(MailTemplateModel,'getTemplateData','invitation'); // MailTemplateModel.getTemplateData shuold have a fn(err,data) as callback // inside getTemplateData, callback with err=new Error('Unable to get email template') if (data.status !== 'success') var unsubscribe_hash = crypto.createHash("md5") .update(dbData.unsubscribe_token + email) .digest('hex'); var unsubscribe_link = app.locals.SITE_URL+'/unsubscribe/' + result.insertId + '/' + unsubscribe_hash; var template_row = res_data.template_row; var user_full_name = user_row.user_firstname+' '+ user_row.user_lastname; var invitation_link = 'http://'+user_row.url_alias+'.'+ app.locals.SITE_DOMAIN; var mailOptions = { "type": 'invitation', "to": dbData.email, "from_name" : user_full_name, "subject": template_row.message_subject .replace('[[USER]]', user_full_name), "text": template_row.message_text_body .replace('[[USER]]', user_full_name) .replace('[[INVITATION_LINK]]', invitation_link) .replace('[[UNSUBSCRIBE_LINK]]', unsubscribe_link), "html": template_row.message_body .replace('[[USER]]', user_full_name) .replace('[[INVITATION_LINK]]', invitation_link) .replace('[[UNSUBSCRIBE_LINK]]', unsubscribe_link) }; mailOptions = JSON.stringify(mailOptions); //send email to queue ... callback(err,data) wait.forMethod(sqsHelper,'addToQueue',cfg.sqs_invitation_url, mailOptions); } catch (e) { // one of the callback returned err!==null emailCallBack(e, email); } } // loop while length>0 completionCallback(); } // run the loop in a Fiber (keep node spinning) wait.launchFiber(processInviteEmails,email_list, user_id, emailCallBack, completionCallback); 

看到? 没有callback地狱

我在我的博客中提出了另一个解决scheme。 这是丑陋的,但它是我可以用纯JavaScript做的最可读的事情。

 var flow1 = new Flow1( { execute_next_step: function(err) { if (err) { console.log(err); }; } } ); flow1.execute_next_step(); function Flow1(parent_flow) { this.execute_next_step = function(err) { if (err) return parent_flow.execute_next_step(err); if (!this.next_step) this.next_step = 'START'; console.log('Flow1:', this.next_step); switch (this.next_step) { case 'START': this.next_step = 'FIRST_ASYNC_TASK_FINISHED'; firstAsyncTask(this.execute_next_step.bind(this)); break; case 'FIRST_ASYNC_TASK_FINISHED': this.firstAsyncTaskReturn = arguments[1]; this.next_step = 'ANOTHER_FLOW_FINISHED'; this.another_flow = new AnotherFlow(this); this.another_flow.execute_next_step(); break; case 'ANOTHER_FLOW_FINISHED': this.another_flow_return = arguments[1]; this.next_step = 'FINISH'; this.execute_next_step(); break; case 'FINISH': parent_flow.execute_next_step(); break; } } } function AnotherFlow(parent_flow) { this.execute_next_step = function(err) { if (err) return parent_flow.execute_next_step(err); if (!this.next_step) this.next_step = 'START'; console.log('AnotherFlow:', this.next_step); switch (this.next_step) { case 'START': console.log('I dont want to do anything!. Calling parent'); parent_flow.execute_next_step(); break; } } }