写这个问题是由于最近看到一些初学者用回调用的不亦乐乎,最后代码左调来又调去很不直观。node
首先上结论:推荐使用async/await或者co/yield,其次是promise,再次是事件,回调不要使用。promise
接下来是解析,为何我会有这样的结论缓存
首先是回调,理解上最简单,就是我把任务分配出去,当你执行完了我就能从你那里拿到结果执行相应的回调,异步
这里演示一个对setTimeout的封装,规定时间后打印相应结果并执行回调函数async
而且这个函数传给回调函数的参数符合node标准,第一个为error信息,若是出错error不为null,正常执行则为null函数
var i = 0; function sleep(ms, callback) { setTimeout(function () { console.log('我执行完啦!'); i++; if (i >= 2) callback(new Error('i大于2'), null); else callback(null, i); }, ms); } sleep(3000, function (err,val) { if(err) console.log('出错啦:'+err.message); else console.log(val); }) //执行结果:3s后打印 "我执行完啦","1"
这样的代码看上去并不会很不舒服,并且也比较好理解,可是假如我要暂停屡次呢ui
调用的代码就变成了以下:spa
sleep(1000, function (err, val) { if (err) return console.log(err.message);; console.log(val); sleep(1000, function (err, val) { if (err) return console.log(err.message); console.log(val); sleep(1000, function (err, val) { if (err) console.log(err.message); else console.log(val); }) }) })
能够看得出来,嵌套得很深,你能够把这三次操做当作三个异步任务,而且还有可能继续嵌套下去,这样的写法显然是反人类的。code
嵌套得深首先一个不美观看的很不舒服,第二个若是回调函数出错了也难以判断在哪里出错的。对象
因而改进方法就是事件监听,每次调用一个异步函数都返回一个EventEmitter对象,并在执行成功时调用done事件,
失败时调用error事件
var i = 0; function sleep(ms) { var emitter = new require('events')(); setTimeout(function () { console.log('我执行完啦!'); i++; if (i >= 2) emitter.emit('error', new Error('i大于2')); else emitter.emit('done', i); }, ms); } var emit = sleep(3000); emit.on('done',function (val) { console.log('成功:' + val); }) emit.on('error',function(err){ console.log('出错了:' + err.message); })
这样写比以前的好处在于能添加多个回调函数,每一个回调函数都能得到值并进行相应操做。但这并无解决回调嵌套的问题,
好比这个函数屡次调用仍是必须写在ondone的回调函数里,看起来仍是很不方便。
因此比较广泛的解决方案是Promise。
promise和事件相似,你能够把它当作只触发两个事件的event对象,可是事件具备即时性,触发以后这个状态就不存在了,这个
事件已经触发过了,你就再也拿不到值了,而promise不一样,promise只有两个状态resolve和reject,当它触发任何一个状态后
它会将当前的值缓存起来,并在有回调函数添加进来的时候尝试调用回调函数,若是这个时候尚未触发resolve或者reject,那么
回调函数会被缓存,等待调用,若是已经有了状态(resolve或者reject),则马上调用回调函数。而且全部回调函数在执行后都当即
被销毁。
代码以下:
var i = 0; //函数返回promise function sleep(ms) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('我执行好了'); i++; if (i >= 2) reject(new Error('i>=2')); else resolve(i); }, ms); }) } sleep(1000).then(function (val) { console.log(val); return sleep(1000) }).then(function (val) { console.log(val); return sleep(1000) }).then(function (val) { console.log(val); return sleep(1000) }).catch(function (err) { console.log('出错啦:' + err.message); })
这个例子中,首先它将本来嵌套的回调函数展开了,如今看的更舒服了,而且因为promise的冒泡性质,当promise链中的任意一个
函数出错都会直接抛出到链的最底部,因此咱们统一用了一个catch去捕获,每次promise的回调返回一个promise,这个promise
把下一个then看成本身的回调函数,并在resolve以后执行,或在reject后被catch出来。这种链式的写法让函数的流程比较清楚了,
抛弃了嵌套,终于能平整的写代码了。
但promise只是解决了回调嵌套的问题,并无解决回调自己,咱们看到的代码依然是用回调阻止的。因而这里就引入了async/await
关键字。
async/await是es7的新标准,而且在node7.0中已经获得支持,只是须要使用harmony模式去运行。
async函数定义以下
async function fn(){ return 0; }
即便用async关键字修饰function便可,async函数的特征在于调用return返回的并非一个普通的值,而是一个Promise对象,若是
正常return了,则返回Promise.resolve(返回值),若是throw一个异常了,则返回Promise.reject(异常)。也就是说async函数的返回
值必定是一个promise,只是你写出来是一个普通的值,这仅仅是一个语法糖。
await关键字只能在async函数中才能使用,也就是说你不能在任意地方使用await。await关键字后跟一个promise对象,函数执行到await后会退出该函数,直到事件轮询检查到Promise有了状态resolve或reject 才从新执行这个函数后面的内容。
首先我用刚刚的例子展现async/await的神奇之处
var i = 0; //函数返回promise function sleep(ms) { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('我执行好了'); i++; if (i >= 2) reject(new Error('i>=2')); else resolve(i); }, ms); }) } (async function () { try { var val; val = await sleep(1000); console.log(val); val = await sleep(1000); console.log(val); val = await sleep(1000); console.log(val); } catch (err) { console.log('出错啦:'+err.message); } } ())
看上去代码是彻底同步的,每等待1s后输出一次,而且在sleep返回的promise中状态为reject的时候还能被try...catch出来。
那么这究竟是怎么回事呢 咱们来看一张图
这段代码和刚刚的代码同样,只是在async函数被调用后输出了一次"主程序没有被调用",结果以下
咱们发现后面输出的话是先打印的,这好像和咱们的代码顺不同,这是怎么回事呢。
总的来讲async/await是promise的语法糖,但它能将本来异步的代码写成同步的形式,try...catch也是比较友好的捕获异常的方式
因此在从此写node的时候尽可能多用promise或者async/await,对于回调就不要使用了,大量嵌套真的很反人类。