Node.js的回调问题2015-06-11 infoq 吴海星Node.js需要按顺序执行异步逻辑时一般采用后续传递风格,也就是将后续逻辑封装在回调函数中作为起始函数的参数,逐层嵌套。这种风格虽然可以提高CPU利用率,降低等待时间,但当后续逻辑步骤较多时会影响代码的可读性,结果代码的修改维护变得很困难。根据这种代码的样子,一般称其为"callback hell"或"pyramid of doom",本文称之为回调大坑,嵌套越多,大坑越深。
坑的起源
后续传递风格
为什么会有坑?这要从后续传递风格(continuation-passing style--CPS)说起。这种编程风格最开始是由Gerald Jay Sussman和Guy L. Steele, Jr. 在AI Memo 349上提出来的,那一年是1975年,Schema语言的第一次亮相。既然JavaScript的函数式编程设计原则主要源自Schema,这种风格自然也被带到了Javascript中。这种风格的函数要有额外的参数:“后续逻辑体”,比如带一个参数的函数。CPS函数计算出结果值后并不是直接返回,而是调用那个后续逻辑函数,并把这个结果作为它的参数。从而实现计算结果在逻辑步骤之间的传递,以及逻辑的延续。也就是说如果要调用CPS函数,调用方函数要提供一个后续逻辑函数来接收CPS函数的“返回”值。
回调
在JavaScript中,这个“后续逻辑体”就是我们常说的回调(callback)。这种作为参数的函数之所以被称为回调,是因为它一般在主程序中定义,由主程序交给库函数,并由它在需要时回来调用。而将回调函数作为参数的,一般是一个会占用较长时间的异步函数,要交给另一个线程执行,以便不影响主程序的后续操作。如下图所示:

在JavaScript代码中,后续传递风格就是在CPS函数的逻辑末端调用传入的回调函数,并把计算结果传给它。但在不需要执行处理时间较长的异步函数时,一般并不需要用这种风格。我们先来看个简单的例子,编程求解一个简单的5元方程:
x+y+z+u+v=16x+y+z+u-v=10x+y+z-u=11x+y-z=8x-y=2
对于x+y=a;x-y=b这种简单的二元方程我们都知道如何求解,这个5元方程的运算规律和这种二元方程也没什么区别,都是两式相加除以2求出前一部分,两式相减除以2求出后一部分。5元方程的前一部分就是4元方程的和值,依次类推。我们的程序写出来就是:代码清单1. 普通解法-calnorm.js
var res = new Int16Array([16,10,11,8,2]),l= res.length;var variables = [];for(var i = 0;i < l;i++) {if(i === l-1) {variables[i] = res[i];}else {variables[i] = calculateTail(res[i],res[i+1]);res[i+1] = calculateHead(res[i],res[i+1]);}}function calculateTail(x,y) {return (x-y)/2;}function calculateHead(x,y) {return (x+y)/2;}