Node.js 中的异步问题
最近工作的时候遇到 Node.js 一些问题,觉得比较有意思,想来记录一下。
平时写惯了 python 代码,习惯了在同步中偶尔插一点异步,初次接触 node.js ,仅仅写一点 CRUD 在对 red-node 开源项目修改的过程中,体会到了在异步中写同步的痛苦(在 callback 中无限套娃),这样对错误的定位会比较麻烦,但是同事提高了整体的运行效率,当然还是自己太菜,接触的太少。
最终在开发的过程中使用了 async 异步模块,可以说是非常的好用了。
首先 JavaScript 中内置的 async/await 已经是 AsyncFunction 特性 中的关键字,目前为止,除了 IE 之外,常用浏览器和 Node (v7.6+) 都已经支持该特性。这是一个非常好的语法糖,解决了在异步中需要同步时陷入 回调地狱 的可能(当然我是非常不太乐意写一堆 callback 嵌套 return 的,前几天有几个下午为了几个尝试给我脑子都套蒙了)。
async/await 干了啥?
async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个 异步方法 执行完成。
async/await是一个整体,await 只能出现在 async 函数中。也就是说,如果调用一个 async 函数使用 await 等待的话,就需要在 await 外面再包一个 async 函数。
async 起什么作用
这个问题的关键在于,async 函数是怎么处理它的返回值的!
我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:
1 | async function testAsync() { |
运行后输出:
1 | Promise { 'hello async' } |
所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。那么 await 其实就是在等一个 Promise 的结果。
Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:
1 | testAsync().then(v => { |
await 到底在等啥
一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。
因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:
1 | function getSomething() { |
await 等到了要等的,然后呢
await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。
如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
如果它等到的是一个 Promise 对象,await 就忙起来了,它会 阻塞 后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
看到上面的阻塞一词,心慌了吧……放心,这就是
await必须用在async函数中的原因。async函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个Promise对象中异步执行。
比较一下使用原生Promise和async/await封装的代码
上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。
现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写:
1 | function takeLongTime() { |
如果改用 async/await 呢,会是这样:
1 | function takeLongTime() { |
眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。
所以总的来说,async/await 可以通过封装 Promise 的方法让我们的代码更像是同步代码,从而解决 then 链套娃的方式,那么这里需要注意的是因为 test() 函数被套上了 async 那么也就是说这个方法是异步的,那么我们如果在 test() 后还有代码执行的话,就会不等待 test() 的结果直接运行下去了,这也就是说,想要等待 test() 的结果,就需要继续套娃 async/await 不过这种方法总比 套娃 callback 看起来更加直观一点。
1 | function takeLongTime() { |
输出结果:
1 | 阿巴阿巴阿巴 |
1 | function takeLongTime() { |
输出结果:
1 | long_time_value |
await等的一定是Promise对象或者另一个async/await。await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
node.js 也有 async 库,当我们调用这个库的时候可以使用其中的一些函数,例如
async.series(tasks, [callback])多个函数依次执行,之间没有数据交换;async.parallel(tasks, [callback])多个函数并行执行;parallelLimit(tasks, limit, [callback])limit个函数并行执行;async.waterfall(tasks, [callback])多个函数依次执行,且前一个的输出为后一个的输入;auto(tasks, [callback])多个函数有依赖关系,有的并行执行,有的依次执行;whilst(test, fn, callback)用可于异步调用的while;
这些函数可以通过 串行 或者 并行 的方式将异步函数同步化,比较适合用于下载和解压的异步操作。