异步编程背景
JavaScript 是单线程的,意味着同一时刻只有一处代码在执行,所以它的线程不能被阻塞住,为了达到这一个目的,JavaScript 使用任务队列的机制来实现异步编程。异步编程一般有两种模式
- 事件模型
- 回调模型
事件模型是指为某事件绑定一个函数,当事件触发时执行此函数,一般在 DOM 编程中比较常见,例如为按钮绑定点击事件
document.getElementById("btn").addEventListener("click", event => { |
回调模型在 Node.js 的 I/O 操作中比较常见,在读取文件时,我们肯定不能阻塞线程等待文件读取完毕,然后进行操作,这里的解决办法是在调用读取文件的函数时,传入一个函数,这个函数在文件被读取完成时会被调用,并且读取到的数据会作为参数传入该函数
const fs = require("fs"); |
当回调函数嵌套较多时,造成代码十分难以阅读,这种情况称之为回调地狱,为了解决这个问题,在 ES6 中提出了 Promise,进一步在 ES7 提出了 async … await 语法。
Promise基础
Promise 的基本语法如下
new Promise(fn1).then(fn2) |
其中 fn1 和 fn2 都是两个函数,fn1 接收两个参数 resolve 和 reject,这两个参数也都是函数,只有当在 fn1 中调用了 resolve 方法后,fn2 方法才会被执行。所以 Promise 可以保证函数执行的顺序,并且 fn1 向 resolve 传入的参数被被传递给 fn2
let promise = new Promise((resolve, reject) => { |
上述代码会在 1s 后打印出 Hello,因为在 1s 后才执行 resolve 函数,此时 fn2 才会被执行,并且 Hello 会作为参数传入 fn2。
现在我们比较一下回调模型与 Promise 模型
const fs = require("fs"); |
let promise = new Promise((resolve, reject) => { |
上面的版本为回调模型版本,下面的版本为 Promise 版本,从写法上看回调版本是一层嵌套一层以此来保证同步性,如果有较多的嵌套的话,代码肯定十分难读,体验感十分不好;而 Promise 的写法则比较像同步的写法(写法上与同步的写法类似,但是实际上还是异步的),即使嵌套再多,可读性也十分的好。
错误处理
如果在 fn1 中出现了错误怎么办,这个时候就需要我们即将介绍的 reject,其实 Promise 还有一种用法
new Promise(fn1).then(fn2, fn3) |
then 可以接收两个函数,当在 fn1 中调用 resolve 方法后,fn2 被执行,当在 fn1 中执行 reject 方法后,fn3 被执行。resolve 表示函数正常执行完毕,用于传递数据,而 reject 表示函数出错,一般用于传递错误,向 reject 传递的参数会被传递给 fn3,所以 fn2 是 fn1 成功执行后的处理逻辑,而 fn3 是 fn1 执行出错后的处理逻辑
let promise = new Promise((resolve, reject) => { |
其实还有一种写法
new Promise(fn1).then(fn2).catch(fn3) |
这种写法与上面的写法是一样的
promise.then(data => { |
链式调用
下面继续介绍 Promise 的新模式,链式调用
new Promise(fn1).then(fn2).then(fn3).then(fn4) |
因为每一个 then 方法都返回一个 Promise 对象,所以可以进行链式的调用
let promise = new Promise((resolve, reject) => { |
上面的程序会依次打印出 1 2 3 4。现在有一个问题是链式调用如何传递值,答案是通过返回值
let promise = new Promise((resolve, reject) => { |
上述的打印结果为
Hello |
我们看一个简单 then 方法的实现来理解上面的逻辑
function then(fn) { |
上面只是为了理解代码的简单实现,并不是 then 方法的真正实现。
但是如果传入 then 中的函数返回的是一个 Promise 对象的话就有所不同,后面链式调用传入 then 中的函数必须等到返回 Promise 执行 resolve 方法之后才会执行,并且向 resolve 传入的值会被传递
let promise = new Promise(resolve => { |
上面 1s 之后会打印出 Hello World。我们进一步改进 then 方法的实现来理解上述的执行结果
function then(fn) { |
finally
在链式调用的最后一般调用 finally 方法,该方法无论上面的链式调用成功执行,或者是链式调用的过程中抛出了错误,传递给 finally 方法的函数最终都会被执行
let promise = new Promise((resolve, reject) => { |
let promise = new Promise((resolve, reject) => { |
由于这一特性,finally 方法一般被用于进行资源的回收。
静态方法
本节讲述 Promise 的静态方法,不过在讲解静态方法之前,我们谈一谈 Promise 对象的状态,Promise 对象有三种状态:
- pending
- resolved(有的地方称为 fulfilled)
- rejected
在调用 resolve 或 reject 方法之前,Promise 对象处于 pending 状态,此时传入 then 中的函数不能被执行,当调用 resolve 方法后,状态由 pending 变为 resolved,当调用 reject 后,状态由 pending 变为 rejected,一旦状态变为 resolved或 rejected 之后,状态不可改变,并且 resolved和 rejected 状态只能由 pending 状态转变而来。
当 Promise 的状态为 resolved时,传入 then 方法的第一个函数就可以执行了,当 Promise 的状态为 rejected 时,传入 then 的第二个函数或者传入 catch 方法的函数就可以执行了。
下面我们介绍 Promise 的静态方法
- Promise.resolve
- Promise.all
- Promise.allSettled
- Promise.race
Promise.resolve
Promise.resolve 返回一个 Promise,根据传入参数的不同,返回不同的值,分为三种情况
- 传入一个 Promise 对象,直接返回该 Promise 对象
let promise = new Promise((resolve, reject) => { |
- 传入一个带有 then 方法的对象,首先执行 then 方法,并且将 resolve 和 reject 传入,返回的 Promise 状态由 then 方法是否调用 resolve 与 reject 方法决定,并且向 resolve 和 reject 传递的参数会被传递
Promise.resolve({ |
- 传入其他值时,返回一个 resolved 状态的 Promise,并且传入的值会作为 resolve 方法的参数被传递
Promise.resolve(3).then(data => { |
经过上面的讲解,可以简单写一个 Promise.resolve 的实现
Promise.resolve = value => { |
Promise.all
Promise.all 接收一个 Promise 对象组成的数组,返回一个 Promise,只有当数组中的所有 Promise 都 resolved 后,返回的 Promise 才会变为 resolved 状态,并且将所有 Promise 传递的结果封装为数组传递下去
let promise1 = new Promise((resolve, reject) => { |
一旦数组中的 Promise 有任一个变为 rejected,返回的 Promise 就会变为 rejected 状态
let promise1 = new Promise((resolve, reject) => { |
所以 all 中的 Promise 要么一起执行成功,要么全部失败,我们拿不到部分成功的 Promise 结果。
Promise.allSettled
Promise.allSettled 方法是解决 Promise.all 一旦一个 Promise 变为 rejected,其他变为 resolved 状态 Promise 传递的结果就仿佛被吞掉了
Promise.allSettled([promise1, promise2]).then(results => { |
Promise.allSettled 方法会返回一个 resolved 状态的 Promise,数组中的 Promise 的执行结果会以对象的形式添加到 results 数组中,status 表示 Promise 最终的状态,value 和 reason 分别表示成功执行时传递的值以及执行出错时传递的错误原因。
Promise.race
Promise.race 方法也是接收一个 Promise 组成的数组,返回一个 Promise,当数组中有任一 Promise 的状态变为 resolved 或者 rejected,返回的 Promise 就会相应的变为 resolved 或 rejected。正如 race 所暗示的,是多个 Promise 在竞争,最终选择最快的那个
let promise1 = new Promise((resolve, reject) => { |
Promise 与生成器
上面使用 Promise 编写的异步代码已经与同步代码很相似了,但是我们可以将 Promise 与 生成器结合起来,使得代码的写法看起来更加的像同步的写法,例如
let promise = new Promise((resolve, reject) => { |
上面的代码是先根据 id 向服务器请求 name,然后根据 name 向服务器请求获得的分数,最后打印出分数,我们希望能有更加同步的写法,例如
asyncTaskRunner(*() => { |
这种写法可以通过 Promise 与 Generator 来做到。asyncTaskRunner 的实现如下
function asyncTaskRunner(generator) { |
在 ES7 中提出了 async … await 语法,它能让我们以同步的方式写出异步代码
(async () =>{ |
只需要在函数参数列表前加入 async 关键字,将 yield 换为 await 即可以同步方式写出优雅的异步代码。