写这个问题是因为最近看到一些初学者用回调用的不亦乐乎,最后代码左调来又调去很不直观。

首先上结论:推荐使用async/await或者co/yield,其次是promise,再次是事件,回调不要使用。

接下来是解析,为什么我会有这样的结论

首先是回调,理解上最简单,就是我把任务分配出去,当你执行完了我就能从你那里拿到结果执行相应的回调,

这里演示一个对setTimeout的封装,规定时间后打印相应结果并执行回调函数

并且这个函数传给回调函数的参数符合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"

这样的代码看上去并不会很不舒服,而且也比较好理解,但是假如我要暂停多次呢

调用的代码就变成了如下:

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

可以看得出来,嵌套得很深,你可以把这三次操作看成三个异步任务,并且还有可能继续嵌套下去,这样的写法显然是反人类的。

嵌套得深首先一个不美观看的很不舒服,第二个如果回调函数出错了也难以判断在哪里出错的。

于是改进方法就是事件监听,每次调用一个异步函数都返回一个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,对于回调就不要使用了,大量嵌套真的很反人类。

node.js异步控制流程 回调,事件,promise和async/await的更多相关文章

  1. Node.js 101(2): Promise and async

    --原文地址:http://blog.chrisyip.im/nodejs-101-package-promise-and-async 先回想一下 Sagase 的项目结构: lib/ cli.js ...

  2. 【Node100In1】01.去异步,解决掉Node.js万恶的回调陷阱

    Node.js是基于事件驱动编程.异步函数随处可见,其中不乏一些常用库的方法.本例就以js中最常见的setTimeout的为例,试图改善一下回调的书写. 先来看一段伪代码: 我们实现一个需求,每隔一段 ...

  3. Node.js实战(九)之事件循环

    Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高. Node.js 几乎每一个 API 都是支持回调函数的. Node ...

  4. Node.js 教程 05 - EventEmitter(事件监听/发射器 )

    目录: 前言 Node.js事件驱动介绍 Node.js事件 注册并发射自定义Node.js事件 EventEmitter介绍 EventEmitter常用的API error事件 继承EventEm ...

  5. node.js中的回调

    同步和阻塞:这两个术语可以互换使用,指的是代码的执行会在函数返回之前停止.如果某个操作阻塞,那么脚本就无法继续,这意味着必须等待. 异步和非阻塞:这两个术语可以互换使用,指的是基于回调的.允许脚本并行 ...

  6. node.js如何使用回调

    Node.js到处使用回调,尤其在有I/O(输入/输出)操作的地方. 下面是在一个Node.js中使用filesystem模块中从磁盘上读入文件内容示例一: var fs = require('fs' ...

  7. node.js异步编程解决方案之Promise用法

    node.js异步编程解决方案之Promise var dbBase = require('../db/db_base'); var school_info_db = require('../db/s ...

  8. Node.js标准的回调函数

    Node.js标准的回调函数:第一个参数代表错误信息,第二个参数代表结果. function (err, data) 当正常读取时,err参数为null,data参数为读取到的String.当读取发生 ...

  9. callback vs async.js vs promise vs async / await

    需求: A.依次读取 A|B|C 三个文件,如果有失败,则立即终止. B.同时读取 A|B|C 三个文件,如果有失败,则立即终止. 一.callback 需求A: let read = functio ...

随机推荐

  1. Unity游戏开发——自动为动画剪辑添加事件 之 最后几帧的事件不能被调用的问题

    最近在做一个根据配置表自动生成动画剪辑clip以及controller的功能.做法是根据配置表配置的动作以及每个动作的关键帧,自动为每个clip添加事件.这样做可以把动画的事件处理在游戏运行之前就计算 ...

  2. HTML 多媒体、Object 元素、音频、视频

    Web 上的多媒体指的是音效.音乐.视频和动画. 现代网络浏览器已支持很多多媒体格式. 什么是多媒体? 多媒体来自多种不同的格式.它可以是您听到或看到的任何内容,文字.图片.音乐.音效.录音.电影.动 ...

  3. jQuery 处理TextArea

    jQuery 处理TextArea: $('#btn001').click(function(e) { console.log("btn001Click"); var cmt = ...

  4. Android系列一、创建项目

    本文是在MAC下的Android Studio操作的. 一.Android入门 1.打开Android Studio,界面如下: 几个选项的意思: 创建一个新的项目 打开一个已经存在的项目 从版本管理 ...

  5. PowerPoint实用知识

    纯手打,可能有错别字,使用的版本是office2013 转载请注明出处 http://www.cnblogs.com/hnnydxgjj/p/6347256.html ,谢谢 母版的使用 制作PPT的 ...

  6. 深圳尚学堂:Java中Class对象

    Java中生成Class对象和生成instance都有多种方式.所以只有弄清其中的原理,才可以深入理解.首先要生成Class对象,然后再生成Instance.那Class对象的生成方式有哪些呢,以及其 ...

  7. list、set、map以及array的区别

    对于刚刚学习集合框架来说,如何选择list.set.map以及array是比较模糊的 在此我将对这四种情况做总结: array:数组,可以存储对象和基本数据类型,长度固定. Collection:集合 ...

  8. XmlHepler(拿去就能用)

    [系列目录](./XmlCatalog.html) 前言 本篇是这个系列的最后一篇.也是本人第一次写系列文章,虽然系列中大部分内容都是参考网络,但是自己都有敲代码验证.自己再编写过程中也学习到了很多知 ...

  9. javascript之自增自减典型运算(易错)

    JavaScript“自增”运算符与表达式 递增运算符(++)是一个一元运算符,该运算符可以将操作数进行递增操作,即每次增加1.递增运算符要求其操作数必须是变量.对象中的某个属性或数组中的某个元素,并 ...

  10. 如何创建DLL文件

    动态链接库(DLL)是从C语言函数库和Pascal库单元的概念发展而来的.所有的C语言标准库函数都存放在某一函数库中.在链接应用程序的过程中,链接器从库文件中拷贝程序调用的函数代码,并把这些函数代码添 ...