node.js异步控制流程 回调,事件,promise和async/await

简介: 写这个问题是因为最近看到一些初学者用回调用的不亦乐乎,最后代码左调来又调去很不直观。首先上结论:推荐使用async/await或者co/yield,其次是promise,再次是事件,回调不要使用。接下来是解析,为什么我会有这样的结论首先是回调,理解上最简单,就是我把任务分配出去,当你执行完了...

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

首先上结论:推荐使用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);
AI 代码解读

}

sleep(3000, function (err,val) {

if(err) console.log('出错啦:'+err.message);
else console.log(val);
AI 代码解读

})

//执行结果: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);
    })
})
AI 代码解读

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

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

于是改进方法就是事件监听,每次调用一个异步函数都返回一个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);
AI 代码解读

}

var emit = sleep(3000);
emit.on('done',function (val) {

console.log('成功:' + val);
AI 代码解读

})
emit.on('error',function(err){

console.log('出错了:' + err.message);
AI 代码解读

})
复制代码
这样写比之前的好处在于能添加多个回调函数,每个回调函数都能获得值并进行相应操作。但这并没有解决回调嵌套的问题,

比如这个函数多次调用还是必须写在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);
})
AI 代码解读

}

sleep(1000).then(function (val) {

console.log(val);
return sleep(1000)
AI 代码解读

}).then(function (val) {

console.log(val);
return sleep(1000)
AI 代码解读

}).then(function (val) {

console.log(val);
return sleep(1000)
AI 代码解读

}).catch(function (err) {

console.log('出错啦:' + err.message);
AI 代码解读

})
复制代码
这个例子中,首先它将原本嵌套的回调函数展开了,现在看的更舒服了,并且由于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;
AI 代码解读

}
即使用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);
})
AI 代码解读

}

(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);
}
AI 代码解读

} ())
复制代码

看上去代码是完全同步的,每等待1s后输出一次,并且在sleep返回的promise中状态为reject的时候还能被try...catch出来。

那么这到底是怎么回事呢 我们来看一张图

这段代码和刚刚的代码一样,只是在async函数被调用后输出了一次"主程序没有被调用",结果如下

我们发现后面输出的话是先打印的,这好像和我们的代码顺不一样,这是怎么回事呢。

总的来说async/await是promise的语法糖,但它能将原本异步的代码写成同步的形式,try...catch也是比较友好的捕获异常的方式

所以在今后写node的时候尽量多用promise或者async/await,对于回调就不要使用了,大量嵌套真的很反人类。

好文要顶 关注我 收藏该文

目录
打赏
0
1
1
0
1
分享
相关文章
如何使用 Promise 处理异步并发操作?
通过使用 `Promise.all()` 和 `Promise.race()` 方法,可以灵活地处理各种异步并发操作,根据不同的业务需求选择合适的方法来提高代码的性能和效率,同时也使异步代码的逻辑更加清晰和易于维护。
除了 Promise.all(),还有哪些方法可以处理异步并发操作?
在上述示例中,`concurrentPromises` 函数接受一个Promise数组和最大并发数作为参数,通过手动控制并发执行的Promise数量,实现了对异步操作的并发控制,并在所有Promise完成后返回结果数组。
如何使用 Promise.all() 处理异步并发操作?
使用 `Promise.all()` 可以方便地处理多个异步并发操作,提高代码的执行效率和可读性,同时通过统一的 `.catch()` 方法能够有效地处理异步操作中的错误,确保程序的稳定性。
JavaWeb JavaScript ③ JS的流程控制和函数
通过本文的详细介绍,您可以深入理解JavaScript的流程控制和函数的使用,进而编写出高效、可维护的代码。
69 32
在 JavaScript 中,如何使用 Promise 处理异步操作?
通过以上方式,可以使用Promise来有效地处理各种异步操作,使异步代码更加清晰、易读和易于维护,避免了回调地狱的问题,提高了代码的质量和可维护性。
掌握现代JavaScript异步编程:Promises、Async/Await与性能优化
本文深入探讨了现代JavaScript异步编程的核心概念,包括Promises和Async/Await的使用方法、最佳实践及其在性能优化中的应用,通过实例讲解了如何高效地进行异步操作,提高代码质量和应用性能。
深度剖析 Vue.js 响应式原理:从数据劫持到视图更新的全流程详解
本文深入解析Vue.js的响应式机制,从数据劫持到视图更新的全过程,详细讲解了其实现原理和运作流程。
用 Promise 处理异步操作的优势是什么?
综上所述,使用Promise处理异步操作能够有效地解决传统回调函数带来的诸多问题,提高代码的质量、可读性、可维护性和可扩展性,是JavaScript中进行异步编程的重要工具和技术。
除了 async/await 关键字,还有哪些方式可以在 JavaScript 中实现异步编程?
【10月更文挑战第30天】这些异步编程方式在不同的场景和需求下各有优劣,开发者可以根据具体的项目情况选择合适的方式来实现异步编程,以达到高效、可读和易于维护的代码效果。
浅谈JavaScript中的Promise、Async和Await
【10月更文挑战第30天】Promise、Async和Await是JavaScript中强大的异步编程工具,它们各自具有独特的优势和适用场景,开发者可以根据具体的项目需求和代码风格选择合适的方式来处理异步操作,从而编写出更加高效、可读和易于维护的JavaScript代码。
54 1

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等