Promise 异步编程
背景
由于浏览器的渲染主线程只有一个,JS 采用了异步的方式避免任务阻塞。从原理上,JS 的异步通过 event loop 实现;而从使用上,JS 的异步主要通过 Promise 实现
对于同步代码,先后执行顺序是固定的,能够得知确切的输入与输出。而在异步代码中,执行完成的之间无法确定,不能保证后续代码执行时已经得到结果,比如 DOM 操作:
1 | function addContent(src) { |
直接按照同步代码的方式编写异步任务时,逻辑会有错误。因此,以往对于异步任务,会给函数传递一个回调函数,在回调函数中执行后续代码。
这种方式在异步任务数量较多时,代码在结构上会层层嵌套,导致代码可读性变得非常差(回调地狱)
作用
Promise 正是为了解决异步编程的问题而提出的一种规范
在此规范中,promise 是一种有状态的对象(pending、fulfilled/rejected),可以通过构造器创建:let proms = new Promise((resolve, reject) => {})
1 | // 有状态有值的对象,格式: |
传递给构造器的参数为一个函数,称为执行器 executor,执行器函数中的代码会在创建 promise 对象时立即同步执行
构造器的两个参数 resolve 和 reject 均为回调函数,分别为执行成功的回调和执行错误的回调
实例方法(ainstance.xxx())
Promise.prototype.then()
对于 Promise 对象本身所代表的异步任务执行完成后,可以通过
.then()执行后续代码.then()中接收两个函数参数:.then(res => {}, err => {}),分别对应 Promise 执行器函数体中的resolve()和rejected()语句,表示异步任务执行成功/失败后的回调。后者可以省略.then(res => {}),而没有前者时,需要设置第一个参数为 null.then(null, err => {})Promise.prototype.catch()
Promise 本身代表的异步任务执行失败时,可以用
.catch()捕获并处理:.catch(err => {}),实际上与.then(null, err => {})等效Promise.prototype.finally()
用于 promise 状态变化后,做一些清理工作。其中的回调函数没有参数:
.finally(() => {}),所以内部无法获取当前 promise 的状态
Promise 能够链式调用,是因为 Promise 实例调用实例方法后,仍然返回一个 Promise 对象,即
let A = xxx.then()、let B = xxx.catch()、let C = xxx.finally()均为 Promise 对象
静态方法(Promise.xxx())
Promise.resolve()
直接创建初始状态为 fulfilled 的 promise 实例:
let A = Promise.resolve(3),等效于let A = new Promise(resolve => resolve(3))Promise.reject()
同上,直接创建初始状态为 rejected 的 promise 实例,
let A = Promise.reject(),等效于let A = new Promise((resolve, reject) => reject())Promise.all()
将多个 promise 实例合并为一个新实例,参数通常为数组:
let B = Promise.all([promiseA, promiseB, promiseC]),返回值 B 为一个新 promise 实例- 如果其中所有 promise 实例均为成功执行 fulfilled 状态,则返回的 promise 实例 B 也为 fulfilled 状态。此时,返回的 promise 实例 B 的值也为同样大小的数组,对应各个 promise 实例的值
- 只要其中 promise 实例中有等待执行 pending 状态,则返回的 promise 实例 B 也为 pending 状态。此时,返回的 promise 实例 B 没有值
- 只要其中 promise 实例中出现执行错误 rejected 状态,则返回的 promise 实例 B 也为 rejected 状态。此时,返回的 promise 实例 B 的值为其中第一个执行错误的 promise 实例的值
Promise.race()
参数和效果与
promise.all()相同,但仅返回其中第一个状态变化的 promise 实例
注意,由于 JS 的单线程,实际执行时,Promise.all() 或 Promise.race() 中数组参数内的 promise 实例还是按照先后顺序开始执行的。但 Promise.all() 返回数组的顺序和实例执行结束的先后无关,而是对应输入数组的顺序;而 Promise.race() 返回的实例就是第一个状态变化的实例,哪怕是第一个开始执行的,不考虑开始执行的先后时差
应用示例
异步加载图片
1 | function loadImageAsync(url) { |
使用 Promise 处理 AJAX 请求
1 | const getJSON = function(url) { |
1 | function fetchData(url) { |
使用 Promise 链式调用处理多个异步任务
1 | function getUser(userId) { |
改进
Promise 的链式调用格式在代码结构上更清晰了,但还不够直观,因此出现了 async/await 语法糖,这种语法糖基于 Promise,但书写格式上使得异步代码看起来类似于同步代码,可读性更强
具体来说,使用 let A = async function xxx() {} 表示一个异步函数,返回值 A 为一个 Promise 对象
在异步函数 async 中,使用 await 表达式表示其中的异步任务,如:
1 | function delay(ms) { |
① async 函数中的异步任务必须使用 await 表达式
② 如果 async 函数中没有 await 表达式,那么 async 是没有必要的
② await 表达式必须在 async 函数中,不能单独使用(例外:ESM 标准中,通过顶层 await 动态导入模块)
实际使用 await 时,尽量将其放在 try {} catch {} 代码块中进行错误捕获。否则 await 的异步任务执行失败时,会直接中断整个 async 函数
如果 async 函数中的多个 await 异步任务之间不存在先后顺序,尽量将这些异步任务并发执行(使用 Promise.all()),提高性能
应用示例
使用 async/await 语法糖处理多个异步任务
1 | async function fetchData() { |
并行执行多个异步操作
1 | async function fetchMultipleData() { |
对比
读取一组 URL,按照读取顺序输出结果
Promise 写法
1
2
3
4
5
6
7
8
9
10
11
12function logInOrder(urls) {
// 远程读取所有URL
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
// 按次序输出
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}Async/await 写法
1
2
3
4
5
6async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}





