# Promise
Promise
是在 ES6
被引入用于简化异步编程的一个语言特性。它是一个对象,表示某个异步操作的结果,因此它有一些状态分别为 pending(待定)
、 fulfill(兑现)
和 reject(拒绝)
。它只可能从 pending->fuifill/reject
,也就是说一旦这个 Promise
被 settle(敲定)
,就不会再被改变了。
# 好处
传统的异步编程如果需要多次回调,会嵌套形成回调地狱,而在下面说到的 Promise
链就可以解决这个问题。其次传统异步编程如果想要捕获异常错误,需要使用回调参数层层传递错误,而 Promise
也可以优雅的解决这个问题。
# 使用
对于一个异步操作例如 fetch
请求网页数据,可能会在此之后进行一些操作比如展示数据。就需要用到 then()
方法,它可以理解为在上次异步操作结束之后会执行这个 then()
。无可避免的是,涉及网络请求有时会发生一些错误,所以 then()
可以传入两个回调函数,第一个在 Promise
兑现时调用,第二个在 Promise
被拒绝 / 抛出异常时调用。但是在日常使用 Promise
时更多会使用 catch()
来处理第二种情况,它其实就是 then(null, )
的一种简写方式,不仅更加语义化并且有下面这个情景的好处。
1 | getJSON('api/user/profile').then(displayUserProfile, handleError); |
第二种写法的好处在于,如果在 displayUserProfile
中抛出了异常,也能被 handleError
处理。
此外还有 finally()
方法,用于在 Promise
被敲定(无论兑现还是拒绝)时调用,它不接受参数并且只会在抛出异常的时候被注意到返回值。一般用于运行一些清理代码。\
# 创建自己的 Promise
上面的操作都建立在可以返回 Promise
的函数上进行操作,我们可以基于构造函数创建属于自己的 Promise
。
1 | new Promise((resolve, reject) => { |
其需要传入两个参数,分别代表解决或拒绝这个 Promise
。为什么说是解决 (resolved) 而不是兑现呢,因为如果你在 resolve()
传入了一个新的 Promise
,将会以这个 Promise
作为最后的返回值,但一般我们会传入一个值,这样就会使用这个值返回并立即兑现。
根据开头提到的,一个 Promise
一旦被敲定就不会再变化了,所以如果代码如下:
1 | new Promise((resolve, reject) => { |
# Promise
的其他方法
# Promise.all()
也可以叫作并行 Promise
,当一些 Promise
它们之间没有相互依赖的时候,你可以并行执行它们以提高效率。
1 | Promise.all([promise1, promise2, ...]) |
它接受一个数组作为参数并输出一个数组作为这些 Promise
的结果,如果数组存在非 Promise
值原封不动的输出。当某个 Promise
被拒绝的时候,会立即返回并输出这个 Promise
拒绝的原因,只有当所有 Promise
都被兑现的时候,才会输出这个数组。
# Promise.allSettled()
它跟上面的 all()
唯一的区别就是,在某个 Promise
拒绝的时候不会直接返回,而是等到所有 Promise
敲定后输出结果。
# Promise.race()
它跟 all () 的区别是,会立即返回第一个兑现 / 拒绝的 Promise
,如果数组中存在非 Promise
会直接返回。
# Promise.any()
正如名字所言,数组中只要有兑现的 Promise
就会返回这个兑现结果,如果全部都被拒绝才会返回错误。
# 顺序串行 Promise
如果你想要让一些 Promise
以某种顺序运行,通常可以使用 .then()
的方法依次向下写,但如果这个数组的内容未知,通常需要自己动态的构建 Promise
链。以下是犀牛书的示例代码:
1 | function fetchSequentially(urls) { |
# async/await
Promise
是用于简化异步编程,而这两个关键字是用于简化 Promise
的使用。它们会让 Promise
看起来同步代码一样,提高代码的可读性。
# 使用
通常情况下 async
和 await
是一同使用的,如果你想使用 await
表达式就必须处于一个 async
函数中,代码如下:
1 | async function getName() { |
原本在 Promise
需要两个 then()
来连接,如果使用 async/await
就可以编写的像同步代码一样优雅。
在较新的 ES
版本中,支持了 顶级await
,即不需要在 async
函数中也可以使用,比如你想在代码的头部加载一个外部模块,这可能是一个外部地址需要用到 fetch
请求,代码如下。并且这个写法只能在 ES
模块文件中使用。
1 | const response = await fetch('https://api.example.com/data'); |
# 并行 await
await
会阻塞当前的异步操作,在上面的代码中是没有问题的,因为 profile
依赖于 response
。但如果是对两个不同的网址进行 fetch
,这个阻塞操作就会降低效率。就可以用到上面的 Promise.all()
。
1 | const res1 = await fetch(url1); |
1 | const [res1, res2] = await Promise.all([fetch(url1), fetch(url2)]); |
# 串行 await
如果想像上面一样串行的执行 Promise
,也可以结合 await
和 for
循环来实现。
1 | async function processPromises(promises) { |
# async
函数细节
async
的函数工作原理可以想象看作这样:
1 | async function f(x) { /* 函数体 */ }; |
# Promise、async/await
语法规则
以下内容可能会在代码输出题中考察。
- 如果进入
catch
并处理抛出这个错误才会停止向下传播,否则就会继续往下传播。 finally
并不代表最后一次操作,在它之后的then
语句依然会执行。then()
如果传入的不是一个函数而是一个数字或Promise
对象,那么会被忽略。await
后面的代码会被放入微任务队列,相当于放入then()
。