JavaScript异步编程之promise与async
异步编程
我们知道,JavaScript语言的执行环境是“单线程”。这种模式的坏处是只要有一个任务耗时很长,后面的任务都必须排队等待。常见的浏览器无响应往往就是因为某一段JS代码长时间运行导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,JavaScript语言将任务的执行模式分成:同步(Synchronous)和异步(Asynchronous)。同步模式就是上面讲到的模式。异步模式则是:每一个任务有一个或多个回调函数(callback)。前一个任务结束后,不是执行后一个任务,而是执行回调函数。后一个任务则是不等前一个任务结束就执行。执行顺序与任务的排列顺序是不一致的、异步的。比如Ajax操作,异步执行http请求。
常见的几种“异步模式”编程
回调函数
这是异步编程最基本的方法。如果f1是一个很耗时的任务,可以考虑改写f1,把依赖f1返回结果的f2写成f1的回调函数。如下:
1 | function f1(callback) { |
优点:简单、容易理解和部署;
缺点:各个部分高度耦合,流程很混乱;每个任务只能制定一个回调函数。
事件监听
事件监听:任务的执行不取决于代码的顺序,而取决于某个事件是否发生。用这种方法修改上面的写法:
1 | // jQuery的写法 |
优点:容易理解;可以绑定多个事件,每个事件可以指定多个回调函数;有利于实现模块化
缺点:整个程序都要变成事件驱动型,运行流程不清晰
发布/订阅
发布/订阅模式(观察者模式):假设存在一个信号中心,某个任务执行完成,就像信号中心发布一个信号(事件),其他任务可以向信号中心订阅这个信号(事件),从而知道什么时候自己可以开始执行。
1 | function f1() { |
与事件监听类似,但优于事件监听。优点:可以通过查看信息中心,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
Promise对象
Promises对象是CommonJS工作组提出的一种规范,Es6将其写进了语言标准,原生提供了Promise
对象。Promise接受一个函数作为参数,该函数的两个参数分别是resolve和reject。这两个函数就是就是回调函数。
- resolve函数:在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
- reject函数:在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then
方法指定resolved
状态和reject
状态的回调函数。
如:优点:回调函数变成了链式写法,使程序流程变得很清楚;如果一个任务已经完成,再添加回调函数,该回调函数会立即执行,所以不用担心是否错过了某个事件或信号。1
2
3
4
5
6
7
8const f1 = new Promise((resolve, reject) => {
if (/* 异步操作成功 */) {
resolve('success')
} else {
reject('fail')
}
})
f1.then(f2)
Promise基本API和常见问题
.then()
1 | Promise.prototype.then(onFulfilled, onRejected) |
对Promise添加onFulfilled
和onRejected
回调,并返回的是一个新的Promise实例,且返回值将作为参数传入这个新Promise的resolve
函数。因此我们可以使用链式写法。如上面的例子。
.catch()
该方法用于指定发生错误时的回调函数。reject
方法的作用,等同于抛错。promise
对象的错误,会一直向后传递,直到被捕获。即错误总会被下一个catch
所捕获。
1 | Promise.prototype.catch(onRejected) |
如果没有使用catch
方法指定处理错误的回调函数,Promise
对象抛出的错误不会传递到外层代码,即不会有任何反应(Chrome会抛错),这是Promise
的另一个缺点。
.all()
该方法用于将多个Promise实例,包装成一个新的Promise实例。Promise.all方法接受一个数组(或具有Iterator接口)作参数,数组中的对象(p1、p2、p3)均为promise实例。
1 | const p = Promise.all([p1, p2, p3]) |
- 当p1, p2, p3状态都变为fulfilled,p的状态才会变为fulfilled,并将三个promise返回的结果,按参数的顺序(而不是 resolved的顺序)存入数组,传给p的回调函数。
- 当p1, p2, p3其中之一状态变为rejected,p的状态也会变为rejected,并把第一个被reject的promise的返回值,传给p的回调函数。
.race()
该方法同样是将多个Promise实例,包装成一个新的Promise实例
不同于all
的是:当p1, p2, p3中有一个实例的状态发生改变(变为fulfilled或rejected),p的状态就跟着改变。并把第一个改变状态的promise的返回值,传给p的回调函数。1
const p = Promise.all([p1, p2, p3])
一些常见的问题
- reject 和 catch 的区别
- promise.then(onFulfilled, onRejected)
在onFulfilled中发生异常的话,在onRejected中是捕获不到这个异常的。 - promise.then(onFulfilled).catch(onRejected)
.then中产生的异常能在.catch中捕获。所以还是建议这种写法
如果在then中抛错,而没有对错误进行处理(即catch),那么会一直保持reject状态,直到catch了错误。如下面的代码,这样写才能保证taskB正常执行
1
2
3
4
5
6promise
.then(taskA)
.catch(onRejectedA)
.then(taskB)
.catch(onRejectedB)
.then(finalTask)每次调用
then
都会返回一个新创建的promise
对象,而then
内部只是返回的数据。
如下面的代码,第一种方法中,then的调用几乎是同时开始执行的,且传给每个then的value都是100,这种方法应当避免。方法二才是正确的链式调用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27//方法1:对同一个promise对象同时调用 then 方法
var p1 = new Promise(function (resolve) {
resolve(100);
});
p1.then(function (value) {
return value * 2;
});
p1.then(function (value) {
return value * 2;
});
p1.then(function (value) {
console.log("finally: " + value);
});
// finally: 100
//方法2:对 then 进行 promise chain 方式进行调用
var p2 = new Promise(function (resolve) {
resolve(100);
});
p2.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log("finally: " + value);
});
// finally: 400在异步回调中抛错,不会被catch到
1
2
3
4
5
6
7
8
9var promise = new Promise((resolve, reject) => {
setTimeout(() => {
throw 'Uncaught Exception!' // 异步回调中抛错
}, 1000);
});
promise.catch(e => {
console.log(e); //This is never called
});promise
状态变为resove或reject,就凝固了,不会再改变
Async/Await
Promise
让我们告别回调函数,写出更优雅的异步代码。但在实践过程中,我们发现Promise也有不完美的地方。ES7中提出了Async/Await,它能使得我们在编写异步代码时变得像同步一样的方式来编写。
规则
async
表示这是一个async
函数,而await
只能在这个函数里面使用。await
表示在这里等待await
后面的操作执行完毕,再执行下一句代码。await
后面紧跟着的最好是一个耗时的操作或者是一个异步操作(当然非耗时的操作也可以的,但是就失去意义了)
与Promise对比
下面是Axios库向GraphQL服务器发送一个请求的例子:
1 | // promise写法 |
可以看到,async/await与Promise一样,是非阻塞的。但async/await使得异步代码看起来像同步代码,这正是它的魔力所在。