深入理解Node.js异步编程:从回调到Async/Await

作者:夜航船时间:2026-10-25 00:24:29浏览:468

本文深入解析Node.js异步编程机制,从单线程模型下的回调函数出发,剖析其错误处理与嵌套导致的回调地狱问题。随后详细阐述ES6 Promise的状态管理与链式调用,并对比介绍Async/Await如何以同步语法简化异步代码,提升可读性与调试效率,帮助开发者掌握现代JS异步处理最佳实践。

文章目录 简介 同步异步和阻塞非阻塞 javascript中的回调 回调函数的错误处理 回调地狱 ES6中的Promise 什么是Promise Promise的特点 Promise的优点 Promise的缺点 Promise的用法 Promise的执行顺序 async和await async的执行顺序 async的特点 总结

简介

由于JavaScript默认采用单线程模型,代码无法创建新线程进行并行执行。然而,在浏览器环境中,仅靠同步执行无法满足点击、鼠标移动等用户交互需求。为此,浏览器提供了一组API,允许JavaScript通过回调机制异步响应页面事件。

进一步地,Node.js引入了非阻塞I/O机制,将异步概念扩展至文件访问和网络调用等领域。

本文将深入探讨各种异步编程方案的优缺点及其发展趋势。

同步异步和阻塞非阻塞

在探讨Node.js异步编程之前,需厘清同步、异步、阻塞和非阻塞这四个易混淆的概念。

阻塞与非阻塞描述的是进程或线程在执行操作或读写数据时,是否需要等待结果,以及在等待期间能否执行其他任务。

若操作需要等待,且等待期间线程或进程无法执行其他任务,只能被动等待,则该操作被称为阻塞。

反之,若进程或线程在执行操作或读写数据的同时还能执行其他任务,则该操作被称为非阻塞。

同步与异步描述的是数据访问方式。同步指需主动读取数据,该过程可能是阻塞或非阻塞的;异步指无需主动读取,而是被动接收通知。

显然,JavaScript中的回调属于被动通知,可称为异步调用。

javascript中的回调

JavaScript中的回调是异步编程的典型示例:


document.getElementById('button').addEventListener('click', () => {
 console.log('button clicked!');
})

上述代码中,我们为按钮添加了click事件监听器。一旦监听到click事件,回调函数将被触发并输出相应信息。

回调函数本质上是一个普通函数,它作为参数传递给addEventListener,仅在事件触发时才会被调用。

前文提到的setTimeout和setInterval实际上也是异步回调函数的应用。

回调函数的错误处理

在Node.js中,如何处理回调中的错误信息?Node.js采用了一种巧妙的方法:任何回调函数的第一个参数均为错误对象。我们可以通过判断该错误对象是否存在来进行相应的错误处理。


fs.readFile('/文件.json', (err, data) => {
 if (err !== null) {
 //处理错误
 console.log(err)
 return
 }

 //没有错误,则处理数据。
 console.log(data)
})

回调地狱

JavaScript的回调虽然优秀,有效解决了同步处理问题,但遗憾的是,若需依赖回调函数的返回值进行下一步操作,便会陷入所谓的“回调地狱”。

虽然“回调地狱”一词略显夸张,但它确实反映了回调函数存在的问题。


fs.readFile('/a.json', (err, data) => {
 if (err !== null) {
 fs.readFile('/b.json',(err,data) =>{
  //callback inside callback
 })
 }
})

如何解决这一问题?

不必担心,ES6引入了Promise,ES2017引入了Async/Await,均可解决此问题。

ES6中的Promise

什么是Promise

Promise是异步编程的一种解决方案,相比传统的“回调函数和事件”,它更为合理且强大。

简而言之,Promise是一个容器,内部保存着某个未来才会结束的事件(通常是异步操作)的结果。

从语法角度看,Promise是一个对象,可通过它获取异步操作的消息。

Promise的特点

Promise具有两个主要特点:

对象的状态不受外界影响。

Promise对象代表一个异步操作,拥有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。

只有异步操作的结果能决定当前状态,其他任何操作均无法改变该状态。

一旦状态改变,便不再变化,任何时候均可获取该结果。

Promise对象的状态改变仅有两种可能:从Pending变为Resolved,或从Pending变为Rejected。

这与事件(Event)完全不同,事件的特点是若错过监听,再去监听则无法得到结果。

Promise的优点

Promise将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。

Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise的缺点

  1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  2. 如果不设置回调函数,Promise内部抛出的错误不会反应到外部。
  3. 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Promise的用法

Promise对象是一个构造函数,用于生成Promise实例:


var promise = new Promise(function(resolve, reject) { 
// ... some code 
if (/* 异步操作成功 */){ 
resolve(value); 
} else { reject(error); } 
}
);

promise可以接then操作,then操作可以接两个function参数,第一个function的参数就是构建Promise的时候resolve的value,第二个function的参数就是构建Promise的reject的error。


promise.then(function(value) { 
// success 
}, function(error) { 
// failure }
);

我们看一个具体的例子:


function timeout(ms){
 return new Promise(((resolve, reject) => {
  setTimeout(resolve,ms,'done');
 }))
}

timeout(100).then(value => console.log(value));

Promise中调用了一个setTimeout方法,并会定时触发resolve方法,并传入参数done。

最后程序输出done。

Promise的执行顺序

Promise一经创建就会立马执行。但是Promise.then中的方法,则会等到一个调用周期过后再次调用,我们看下面的例子:


let promise = new Promise(((resolve, reject) => {
 console.log('Step1');
 resolve();
}));

promise.then(() => {
 console.log('Step3');
});

console.log('Step2');

输出:
Step1
Step2
Step3

async和await

Promise当然很好,我们将回调地狱转换成了链式调用。我们用then来将多个Promise连接起来,前一个promise resolve的结果是下一个promise中then的参数。

链式调用有什么缺点呢?

比如我们从一个promise中,resolve了一个值,我们需要根据这个值来进行一些业务逻辑的处理。

假如这个业务逻辑很长,我们就需要在下一个then中写很长的业务逻辑代码。这样让我们的代码看起来非常的冗余。

那么有没有什么办法可以直接返回promise中resolve的结果呢?

答案就是await。

当promise前面加上await的时候,调用的代码就会停止直到 promise 被解决或被拒绝。

注意await一定要放在async函数中,我们来看一个async和await的例子:


const logAsync = () => {
 return new Promise(resolve => {
 setTimeout(() => resolve('小马哥'), 5000)
 })
}

上面我们定义了一个logAsync函数,该函数返回一个Promise,因为该Promise内部使用了setTimeout来resolve,所以我们可以将其看成是异步的。

要是使用await得到resolve的值,我们需要将其放在一个async的函数中:


const doSomething = async () => {
 const resolveValue = await logAsync();
 console.log(resolveValue);
}

async的执行顺序

await实际上是去等待promise的resolve结果我们把上面的例子结合起来:


const logAsync = () => {
 return new Promise(resolve => {
  setTimeout(() => resolve('小马哥'), 1000)
 })
}

const doSomething = async () => {
 const resolveValue = await logAsync();
 console.log(resolveValue);
}

console.log('before')
doSomething();
console.log('after')

上面的例子输出:

before after 小马哥

可以看到,aysnc是异步执行的,并且它的顺序是在当前这个周期之后。

async的特点

async会让所有后面接的函数都变成Promise,即使后面的函数没有显示的返回Promise。


const asyncReturn = async () => {
 return 'async return'
}

asyncReturn().then(console.log)

因为只有Promise才能在后面接then,我们可以看出async将一个普通的函数封装成了一个Promise:


const asyncReturn = async () => {
 return Promise.resolve('async return')
}

asyncReturn().then(console.log)

总结

promise避免了回调地狱,它将callback inside callback改写成了then的链式调用形式。

但是链式调用并不方便阅读和调试。于是出现了async和await。

async和await将链式调用改成了类似程序顺序执行的语法,从而更加方便理解和调试。

我们来看一个对比,先看下使用Promise的情况:


const getUserInfo = () => {
 return fetch('/users.json') // 获取用户列表
 .then(response => response.json()) // 解析 JSON
 .then(users => users[0]) // 选择第一个用户
 .then(user => fetch(`/users/${user.name}`)) // 获取用户数据
 .then(userResponse => userResponse.json()) // 解析 JSON
}

getUserInfo()

将其改写成async和await:


const getUserInfo = async () => {
 const response = await fetch('/users.json') // 获取用户列表
 const users = await response.json() // 解析 JSON
 const user = users[0] // 选择第一个用户
 const userResponse = await fetch(`/users/${user.name}`) // 获取用户数据
 const userData = await userResponse.json() // 解析 JSON
 return userData
}

getUserInfo()

可以看到业务逻辑变得更加清晰。同时,我们获取到了很多中间值,这样也方便我们进行调试。

到此这篇关于深入理解nodejs中的异步编程的文章就介绍到这了,更多相关nodejs异步编程内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

作者最新文章