第4章 书写异步代码
为了保证执行是串行的,将下一个异步操作放到上一个异步操作的回调方法里,当代码嵌套的层数增加,代码的层次结构就会不清晰并且难以维护,这种写法被描述为回调地狱(callback hell)
。
4.1 异步操作的返回值
希望通过简单的函数调用拿到异步操作的返回值,但是外部方法会先于回调函数返回。
4.2 组织回调方法
回调与CPS
将回调函数
作为参数
传递,这种书写方式通常被称为Continuation Passing Style(CPS),
它的本质仍然是一个高阶函数
。只是避免了多个回调函数在形式上嵌套在一起。
使用async模块简化回调
async
是一个著名的第三方模块
,根据业务场景提供了一系列常用的方法:
async.series
series方法接受一个数组和一个回调函数,回调函数的第二个参数是一个数组,包含了所有异步操作的返回结果,结果集中的顺序和series参数数组的顺序是对应的。所有的异步调用都是顺序执行的。async.parallel
调用方式和参数都与series相同,也会顺序返回所有的调用结果,区别在于所有的方法是并行执行,执行时间由耗时最长的调用决定。async.waterfall
同样是顺序执行异步操作,和前两个方法的区别是每一个异步操作都会把结果传递给下一个调用。async.map
map接收一个数组作为参数,数组的元素为方法的参数,数组里的值会依次传递给定义的异步方法。map的第二个参数就是异步的方法。
map的缺点
就是它只能接受三个参数,分别是一个数组,对应的异步方法和回调函数,没有多余的参数给异步方法使用,需要对异步方法进一步封装。
4.3 使用Promise
Promise是什么
可以将Promise理解为一个状态机
,它存在下面三种不同的状态,并在某一个时刻只能有一种状态。
Pending
:表示还在执行。Fulfilled
(或者resolved
):执行成功。Rejected
:执行失败。
Promise的状态只能从Pending转换为Resolved或者由Pending转换为Rejected,一旦状态转换完成就无法再改变。
若我们用一个Promise封了一个异步操作,那么当它被创建的时候就处于Pending状态,当异步操作成功完成时,我们将状态转换为Fulfilled;如果执行中出现错误,将状态转换为Rejected。
ES2015中的Promise
- 将异步方法封装成Promise,在回调函数中针对不同的结果调用resolve或者reject方法即可。
- 使用
then
方法获取结果
then方法接收两个匿名函数作为参数,它们代表onResolved
和onRejected
函数。
promise.then(function(value) {
//success
}, function(error) {
// failure
});
通常来说,如果onRejected
的回调方法被调用就表示异步过程中出现错误,这时可以使用catch
方法而不是回调函数来处理异常。
promise
.then(function(data) {
//success
})
.catch(function(err) {
//error
});
- then方法的返回值
then方法总是返回一个新的Promise对象
,默认返回的Promise是一个空的对象,开发者可以在then方法中返回一个新的Promise - Promise的执行
Promise从被创建的那一刻起就开始执行,then方法只是提供了访问Promise状态的接口,与Promise的执行无关。
Promise的常用API
Promise.resolve
resolve方法用来将一个非Promise对象转化为Promise对象,能转换的通常只有thenable
对象和一些原始类型
的对象,常量或不具备状态的语句,转换后的对象自动处于resolve状态,转换的对象作为resolved的结果原封不动地保留。转换异步方法,需要手动封装一个Promise;Promise.reject()
promise.rejected同样会返回一个Promise对象,不同之处在于这个Promise的状态为reject,reject方法的参数会作为错误信息传递给then方法;Promise.all()
将多个promise对象包装成一个Promise。接收一个数组作为参数,只有数组中的Promise状态全部变成resolved之后,all方法生成的Promise状态才会变成resolved;如果中间有一个Promise状态为rejected,那么转换后的Promise也会变成rejected,并将错误信息传给catch;all的结果是按照顺序排列的,只是对最后结果做一下包装。Promise.race()
race方法接收一个Promise数组作为参数并返回一个新的Promise,数组中的Promise会同时执行,race返回的Promise的状态由数组中率先执行完毕的promise的状态决定。
在处理web服务器超时逻辑时十分方便。Promise.catch
Promise在执行时如果出现了错误,可以使用throw关键字
抛出错误,也可以直接使用reject方法
,并且使用catch方法
捕获;
第三方模块的Promise
bluebird
4.4 Generator,一种过渡方案
Generator函数可以由用户执行中断或者恢复执行的操作,Generator中断后可以转去执行别的操作,然后再回过头从中断的地方恢复执行。
Generator的使用
Generator函数和普通函数在外表上最大的区别:
- 在function关键字和方法名中间有个
星号(*)
。 - 方法体中使用
“yield”关键字
。
// 普通方法形式
function* generator() {}
//函数表达式
var gen = function* generator() {}
// 对象的属性方法
var obj = {
* generator () {}
}
yield关键字用来定义函数执行的状态
,如果Generator中定义了x个yield关键字,那么就有x+1种状态。(+1是因为最后的return语句)
Generator函数的执行
当调用Generator函数之后,该函数并没有立刻执行,函数的返回结果是一个对象
,可以理解该对象为一个指针,指向Generator函数当前的状态。调用next
方法后,指针向下移动,最后停在下一个遇到yield关键字
。
- 调用next方法,每次都返回一个包含执行信息的对象,包含一个表达式的值和一个标记执行状态的flag。
next可以接收一个数值作为参数
,代表上一个yield求值的结果。 - next方法与Iterator接口
Generator函数会返回一个对象,而该对象实现一个Iterator接口
,因此能够遍历Iterator接口的方法都可以用来执行Generator,例如for/of
、array.from()
等。for/of循环会在done属性为true时停止。
Generator中的错误处理
Generator函数的原型中定义了throw方法
,用于抛出异常。
Generator对象抛出异常,然后被函数体中try/catch
;如果Generator函数在执行中出错,也可以在外部捕获;
Generator的原型对象还定义了return()方法
,用来结束一个Generator函数的执行,return()方法后面的next不会被执行;
用Generator组织异步方法
之所以可以使用Generator函数来处理异步任务,原因:
- Generator函数可以
中断和恢复执行
,这个特性由yield关键字
来实现; - Generator函数
内外可以交换数据
,这个特性由next函数
来实现;
处理异步的核心思想
:先把函数暂停在某处,然后拿到异步操作的结果,然后再将这个结果传到方法体内。
Generator的自动执行
- 自动执行器的实现
- 基于Promise的执行器
- 使用co模块来自动执行
4.5 回调的终点——async/await
async函数的概念
ES2017标准引入了async函数
,可以看作是自带执行器的Generator函数
。
var asyncReadFile = async function () {
var result1 = await readFile('foo.txt');
var result2 = await readFile('bar.txt');
};
yield关键字换成了await,方法名前的*号变成了async关键字;
使用上的区别是await关键字后面往往是一个Promise,如果不是就隐式调用promise.resolve来转换成一个Promise。await——等待后面的Promise执行完成后再进行下一步操作。
另一个重要区别在于调用形式,可以直接通过方法名来调用。
- 声明一个async方法
// 普通的函数声明
async function foo () {}
//声明一个函数表达式
const foo = async function () {}
// async形式的箭头函数
const foo = async () => {}
- async的返回值
async函数总是会返回一个Promise对象,如果return关键字后面不是一个Promise,那么会默认调用promise.resolve方法进行转换。 - async函数的执行过程
(1) 在async函数开始执行的时候,会自动生成一个Promise对象
。
(2)当方法开始执行后,如果遇到return关键字
或者throw关键字
,执行会立即退出,如果遇到await关键字
则会暂停执行(await后面的异步操作结束后会恢复执行)。
(3)执行完毕,返回一个Promise
。
await关键字
await可以“自动执行”一个Promise(其实是等后面的Promise完成后再进行下一个动作);
await操作符的结果
是由其后面Promise对象的操作结果来决定的。
- await与并行
当异步操作之间不存在结果的依赖关系时,可以使用promise.all()
来实现并行。 - 错误处理
当async函数中有多个await关键字时,如果有一个await的状态变成了rejected
,那么后面的操作就不会继续执行,最好用try/catch
将所有的await包裹起来。
async函数的缺点
最底层的异步操作被封装了async方法,那么该函数的所有上层方法可能都要变成async方法了。