ES6 中的 Promise 是异步编程的一种方案。从语法上讲,Promise 是一个对象,它可以获取异步操作的消息。
Promise 对象, 可以用同步的表现形式来书写异步代码(也就是说,代码看起来是同步的,但本质上的运行过程是异步的)。使用 Promise 主要有以下好处:
(1)通过 new Promise()
构造出一个 Promise 实例。Promise 的构造函数中传入一个参数,这个参数是一个函数,该函数用于处理异步任务。
(2)函数中传入两个参数:resolve 和 reject,分别表示异步执行成功后的回调函数和异步执行失败后的回调函数。代表着我们需要改变当前实例的状态到已完成或是已拒绝。
(3)通过 promise.then() 和 promise.catch() 处理返回结果(这里的 promise
指的是 Promise 实例)。
Promise 的自带 API 提供了如下实例方法:
promise.then()
:获取异步任务的正常结果。promise.catch()
:获取异步任务的异常结果。promise.finaly()
:异步任务无论成功与否,都会执行。Promise 的自带 API 提供了如下静态方法:
Promise.resolve()
Promise.reject()
Promsie.all()
:并发处理多个异步任务,所有任务都执行成功,才算成功(走到 resolve);只要有一个失败,就会马上走到 reject,整体都算失败。Promise.race()
:并发处理多个异步任务,返回的是第一个执行完成的 promise,且状态和第一个完成的任务状态保持一致。Promise.allSettled()
:并发处理多个异步任务,返回所有任务的执行结果(包括成功、失败)。当你有多个彼此不依赖的异步任务执行完成时,或者你想知道每个 promise 的结果时,通常使用它。Promise.all()
Promise.any()
看到这里,你估计还是不知道 Promise 怎么使用。我们不妨来看一下 Promise 有哪些状态,便一目了然。要知道,Promise 的精髓在于对异步操作的状态管理。
1、promise 有三种状态:等待中、成功、失败。等待中状态可以更改为成功或失败,已经更改过状态后⽆法继续更改(例如从失败改为成功)。
2、promise 实例中需要传⼊⼀个函数,这个函数接收两个参数,执⾏第⼀个参数之后就会改变当前 promise 为「成功」状态,执⾏第⼆个参数之后就会变为「失败」状态。
3、通过 .then ⽅法,即可在上⼀个 promise 达到成功时继续执⾏下⼀个函数或 promise。同时通过 resolve 或 reject 时传⼊参数,即可给下⼀个函数或 promise 传⼊初始值。
4、失败的 promise,后续可以通过 promise 自带的 .catch ⽅法或是 .then ⽅法的第⼆个参数进⾏捕获。
resolve();
之后,就会立刻把 .then()
里面的代码加入到微任务队列当中。setTimeout(() => {
// 宏任务
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
resolve();
console.log('promise1'); // 同步任务
}).then((res) => {
// 微任务
console.log('promise then');
});
console.log('qianguyihao'); // 同步任务
打印结果:
promise1
qianguyihao
promise then
setTimeout
上方代码执行的顺序依次是:同步任务 --> 微任务 --> 宏任务。
new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
console.log('setTimeout'); // 宏任务
}, 0);
console.log('promise1');
}).then((res) => {
// 微任务
console.log('promise then');
});
console.log('qianguyihao');
打印结果:
promise1
qianguyihao
setTimeout
promise then
上方代码解释:在执行宏任务的过程中,创建了一个微任务。但是需要先把当前这个宏任务执行完,再去创建并执行微任务。
async/await 是在 ES8(即ES 2017)中引入的新语法,是另外一种异步编程解决方案。
本质: 是 Generator 的语法糖。
我们在普通的函数前面加上 async 关键字,就成了 async 函数。
什么是语法糖呢?语法糖就是让语法变得更加简洁、更加舒服,有一种甜甜的感觉。
async function testAsync() {
return "hello async";
}
const result = testAsync();
console.log(result); // 输出的是一个 Promise 对象。
所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象。如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样
testAsync().then(v => {
console.log(v); // 输出 hello async
});
一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。所以下面这个示例完全可以正确运行:
function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test();
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。
/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 200,这个值将用于下一步骤
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
现在用 Promise 方式来实现这三个步骤的处理
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms
输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。
如果用 async/await 来实现呢,会是这样
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样。
Promise 方案的死穴—— 参数传递太麻烦了
现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}
function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
}
这回先用 async/await 来写:
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms
除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => {
return step2(time1, time2)
.then(time3 => [time1, time2, time3]);
})
.then(times => {
const [time1, time2, time3] = times;
return step3(time1, time2, time3);
})
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
那一堆参数处理,有没有感觉有点复杂的样子?这时候就更能体现 async/await 的好处了吧?
实际开发中,现在有三个网络请求,请求2必须依赖请求1的结果,请求3必须依赖请求2的结果,如果按照往常的写法,会有三层回调,会陷入“回调地狱”。
这种场景其实就是接口的多层嵌套调用。之前学过 Promise,它可以把原本的多层嵌套调用改进为链式调用。
而今天要学习的 async/await ,可以把原本的“多层嵌套调用”改成类似于同步的写法,非常优雅。
我们在使用 Promise、async…await、Generator 的时候,返回的都是 Promise 的实例。
如果直接使用 Promise,则需要通过 then 来进行链式调用;如果使用 async…await、Generator,写起来更像同步的代码。