写点什么

深入理解 nodejs 中的异步编程

发布于: 2021 年 01 月 16 日

简介

因为 javascript 默认情况下是单线程的,这意味着代码不能创建新的线程来并行执行。但是对于最开始在浏览器中运行的 javascript 来说,单线程的同步执行环境显然无法满足页面点击,鼠标移动这些响应用户的功能。于是浏览器实现了一组 API,可以让 javascript 以回调的方式来异步响应页面的请求事件。


更进一步,nodejs 引入了非阻塞的 I/O ,从而将异步的概念扩展到了文件访问、网络调用等。


今天,我们将会深入的探讨一下各种异步编程的优缺点和发展趋势。


同步异步和阻塞非阻塞

在讨论 nodejs 的异步编程之前,让我们来讨论一个比较容易混淆的概念,那就是同步,异步,阻塞和非阻塞。


所谓阻塞和非阻塞是指进程或者线程在进行操作或者数据读写的时候,是否需要等待,在等待的过程中能否进行其他的操作。


如果需要等待,并且等待过程中线程或进程无法进行其他操作,只能傻傻的等待,那么我们就说这个操作是阻塞的。


反之,如果进程或者线程在进行操作或者数据读写的过程中,还可以进行其他的操作,那么我们就说这个操作是非阻塞的。


同步和异步,是指访问数据的方式,同步是指需要主动读取数据,这个读取过程可能是阻塞或者是非阻塞的。而异步是指并不需要主动去读取数据,是被动的通知。


很明显,javascript 中的回调是一个被动的通知,我们可以称之为异步调用。


javascript 中的回调

javascript 中的回调是异步编程的一个非常典型的例子:


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

上面的代码中,我们为 button 添加了一个 click 事件监听器,如果监听到了 click 事件,则会出发回调函数,输出相应的信息。


回调函数就是一个普通的函数,只不过它被作为参数传递给了 addEventListener,并且只有事件触发的时候才会被调用。


上篇文章我们讲到的 setTimeout 和 setInterval 实际上都是异步的回调函数。


回调函数的错误处理

在 nodejs 中怎么处理回调的错误信息呢?nodejs 采用了一个非常巧妙的办法,在 nodejs 中,任何回调函数中的第一个参数为错误对象,我们可以通过判断这个错误对象的存在与否,来进行相应的错误处理。


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 有两个特点:


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

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


只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。


  1. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

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');
输出:Step1Step2Step3
复制代码

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')
复制代码

上面的例子输出:


beforeafter小马哥
复制代码

可以看到,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()
复制代码

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


本文作者:flydean 程序那些事

本文链接:http://www.flydean.com/nodejs-async/

本文来源:flydean 的博客

欢迎关注我的公众号:「程序那些事」最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!


发布于: 2021 年 01 月 16 日阅读数: 76
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
深入理解nodejs中的异步编程