nodejs中的异步问题

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
2
3
4
5
6
async function testAsync() {
return "hello async";
}

const result = testAsync();
console.log(result);

运行后输出:

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
2
3
testAsync().then(v => {
console.log(v); // 输出 hello async
});

await 到底在等啥

  一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

  因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getSomething() {
return "something";
}

async function testAsync() {
return Promise.resolve("hello async");
}

async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}

test();

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
2
3
4
5
6
7
8
9
function takeLongTime() {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}

takeLongTime().then(v => {
console.log("got", v);
});

  如果改用 async/await 呢,会是这样:

1
2
3
4
5
6
7
8
9
10
11
12
function takeLongTime() {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}

async function test() {
const v = await takeLongTime();
console.log(v);
}

test();

  眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

  所以总的来说,async/await 可以通过封装 Promise 的方法让我们的代码更像是同步代码,从而解决 then 链套娃的方式,那么这里需要注意的是因为 test() 函数被套上了 async 那么也就是说这个方法是异步的,那么我们如果在 test() 后还有代码执行的话,就会不等待 test() 的结果直接运行下去了,这也就是说,想要等待 test() 的结果,就需要继续套娃 async/await 不过这种方法总比 套娃 callback 看起来更加直观一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
function takeLongTime() {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}

async function test() {
const v = await takeLongTime();
console.log(v);
}

test();
console.log("阿巴阿巴阿巴")

输出结果:

1
2
阿巴阿巴阿巴
long_time_value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function takeLongTime() {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}

async function test() {
const v = await takeLongTime();
console.log(v);
}

async function run(){
await test();
console.log("阿巴阿巴阿巴")
}

run()

输出结果:

1
2
long_time_value
阿巴阿巴阿巴

await 等的一定是 Promise 对象或者另一个 async/awaitawait 命令后面的 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;

  这些函数可以通过 串行 或者 并行 的方式将异步函数同步化,比较适合用于下载和解压的异步操作。