js基础之异步编程解决方案

异步编程是JS中必备的一部分。由于JS是单线程,如果全部事件都顺序执行会阻塞进程。所以一些耗时较长的事件采用异步方式进行。这里记录一些《深入浅出Node.js》笔记,有些地方没有碰到过,不理解,后续完善。

异步编程的一些问题:

  1. 异常处理
    一般,使用try/catch/finally来进行错误处理
1
2
3
4
5
6
7
try {
JSON.parse(json);
} catch (e) {
console.error(e);
} finally { // 无论是否发生错误,finally都会执行。错误会在最近的catch块中捕获,然后终止
console.log("finally");
}

现在有一个异步方法:

1
2
3
4
5
6
7
8
const async = function (callback) {
process.nextTick(callback);
}
try {
async(callback);
} catch (e) {
console.error(e);
}

这里调用async()方法只能捕获async方法的异常,但是回调callback的异常却无法捕获

  1. 函数嵌套太深
1
2
3
4
5
6
7
8
9
10
11
12
13
connection.query(sql, (err, result) => {
if(err) {
console.err(err)
} else {
connection.query(sql, (err, result) => {
if(err) {
console.err(err)
} else {
...
}
})
}
})
  1. 阻塞代码
1
2
3
4
5
// 这段代码虽然可以阻塞进程,但会持续占用CPU进行判断,性能不佳
const start = new Date();
while (new Date() - start < 1000) {
···
}
  1. 多线程编程
  2. 异步转同步

异步编程解决方案

事件发布/订阅模式

1
2
3
4
5
6
// 监听
emitter.on('message', function(msg) {
console.log(msg);
})
// 发布
emitter.emit('message', 'I\'m bat man!');

事件发布/订阅没有同步、异步的概念,只要一发布、监听者立刻调用。

Promise/Defeered模式

Defeered: 延迟对象,暂不处理。

Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及该异步操作的结果值。它的特点:

  1. Promise对象代表一个异步操作,只有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
  2. Promise只会从pending转化到fulfilled或者是rejected,不会逆转。转化成功后就不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。

基本用法

创建一个Pormise实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function asyncTime() {
return new Promise((resolve, reject) => {
console.log('promise start');
setTimeout(() => {
resolve('time down')
}, 5000);
});
}
asyncTime()
.then((res) => {
console.log(res);
})
.catch((error) => {
console.error(error);
})
.finally(() => { // finally方法的回调函数不接受任何参数
console.log('执行完成');
})

输出结果:

1
2
3
promise start
// 中间间隔5秒
time down

Promise实例化后立即执行,输出”promise start”,5秒后执行成功,输出”time down”。

有几点需要注意:

  1. resolve将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果作为参数传递出去。reject也可以将Promise对象的状态结束,但是是变成“失败”(即从 pending 变为 rejected)。
  2. then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。一般then只用来接收resolve状态。rejected状态由catch捕获执行。
  3. resolve()、reject()、then()和cacth()方法的返回值都是新的Promise对象。所以可以在后面接着使用then/catch,链式调用。
  4. 立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
1
2
3
4
5
6
7
8
9
10
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three

链式调用

1
2
3
4
5
doSomething()
.then(result => doSomethingElse(value))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch((err) => {console.error(err)});

通常,一遇到异常抛出,promise链就会停下来,直接调用链式中的catch处理程序。

单独使用Promise.resolve()和Promise.reject()

Promise.resolve() 和 Promise.reject() 是手动创建一个已经resolve或者reject的promise快捷方法。以Promise.resolve()为例

1
2
3
4
5
Promise.resolve('foo')
// 等价于
new Promise((resolve) => {
resolve('foo');
})
  1. 如果Promise.resolve()参数是一个Promise对象,则不做任何修改,直接返回;
  2. 如果Promise.resolve()参数是一个不具有then方法的对象或根本就不是对象的参数,返回一个resolve状态的Promise对象
  3. 如果Promise.resolve()不带参数,返回一个resolve状态的Promise对象
  4. Promise.reject()返回reject状态的Promise对象

Primse.all()和Promise.race()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

1
const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实>例。p的状态由p1、p2、p3决定,分成两种情况:

  1. 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  2. 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

1
const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

async函数

async函数跟Promise可以搭配使用。async函数有返回值时会返回一个Promise,后面可以跟then方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 基本用法,start → timer middle → (timer then) → end
async function test() {
console.log('start');
try {
await timer();
// await timer().then(() => {
// console.log('timer then');
// });
} catch (err) {
console.log(err);
}
console.log('end');
}
function timer() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('timer middle');
resolve();
}, 3000)
})
}
test();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误示范,新建Promise会立即运行,而且没有返回一个Promise对象,await后面要么跟一个Promise对象(或有then方法的对象)要么跟个常量,
// 这里timer()先执行,后面再执行test()时会报错:timer is not a function。
async function test() {
console.log('start');
await timer();
console.log('end');
}
const timer = new Promise((resolve) => {
console.log('timer start');
setTimeout(() => {
console.log('middle');
resolve();
}, 3000);
})
test();

异步的并发限制和超时控制

异步解决方案成熟的第三方库有async、Step等。

  • Node中的异步调用有时需要控制并发数量,防止底层系统的性能出问题,一种思路是创建一个队列,每个异步调用顺序存入。设定最大并发数,如果当前活跃的异步调用数量小于最大并发数,直接取出执行,如果大于最大数量,则暂存在队列中,顺序取出调用。
  • 超时控制可以给异步调用设置一个时间阈值,如果异步调用没有在规定时间内完成,则提示超时。

参考资料

《深入浅出Node.js》
ECMAScript6入门——Promise对象