5.2 Promise
Promise 是 JavaScript 中的一种编程模式,用于处理异步操作 。它提供了一种更加优雅的方式来组织异步代码,避免了回调地狱 (callback hell)的问题,并且使得错误处理更加一致。Promise 对象代表了一个最终会在未来完成(或失败)的异步操作,所以在 Promise 返回给调用者的时候,操作往往还没有完成,并且其结果值未知。
5.2.1 回调地狱
- 回调函数:当一个函数作为参数传入另一个参数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数。我们熟悉的定时器和Ajax中就存在有回调函数:
javascript
//function(){console.log('执行了回调函数')}就是回调函数,它只有在3秒后才会执行
setTimeout(function(){
console.log('执行了回调函数');
},3000) //3000毫秒
- 异步任务:与之相对应的概念是"同步任务",同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行下一个任务。异步任务不进入主线程,而是进入异步队列,前一个任务是否执行完毕不影响下一个任务的执行。同样,还拿定时器作为异步任务举例:
javascript
setTimeout(function(){
console.log('执行了回调函数');
},3000)
console.log('111');
上述代码的实际输出顺序为:"111"、"执行了回调函数"。
- 回调地狱:
根据前面我们可以得出一个结论:存在异步任务的代码,不能保证能按照顺序执行,那如果我们非要代码顺序执行呢?比如我需要先输出"First",再输出"Second",再输出"Third"。我必须要这样操作,才能保证顺序正确:
javascript
setTimeout(function () {
console.log("First");
setTimeout(function () {
console.log("Second");
setTimeout(function () {
console.log("Third");
}, 2000);
}, 2000);
}, 2000);
可以看到,代码中的回调函数套回调函数,居然套了3层,这种回调函数中嵌套回调函数的情况就叫做回调地狱。
实际上,回调地狱的场景还是挺多见的,我们经常需要先发送一段请求查询到某些数据以当做参数传递给下一个请求,这样下一个请求就必须等上一个请求完毕后才能执行。
5.2.2 Promise的使用
Promise 是异步编程的一种解决方案,主要是用来解决回调地狱的问题。
Promise 构造函数接受一个函数作为参数,我们称该函数为起始函数。Promise创建后起始函数会立即执行 ,并且是同步执行,这意味着后面的代码必须要等待起始函数的任务执行完毕后才会执行。
示例代码:
javascript
// 使用while循环,模拟延迟
function sleep(delay) {
let start = new Date().getTime();
while (new Date().getTime() - start < delay) {
continue;
}
}
// 创建一个 Promise 对象
const promise = new Promise(function (resolve, reject) {
console.log('Promise start...');
// 模拟延迟
sleep(2000)
console.log('Promise end...');
});
console.log('程序执行完毕');
程序执行效果:

起始函数包含两个函数 resolve 和 reject,分别表示 Promise 成功和失败的状态。在起始函数执行成功时,它应该调用 resolve 函数并传递成功的结果。当起始函数执行失败时,它应该调用 reject 函数并传递失败的原因。
Promise 对象一旦 Promise 被创建,它的状态会在"待定"(pending)、"兑现"(fulfilled)和"拒绝"(rejected)之间转变。
- 待定状态(pedding):调用promise时(创建Promise对象时),一开始就呈现出等待状态,遇到resolve或者reject之前,都处于这个状态。
- 兑现状态(fulfilled):在执行了resolve后,promise则会从待定状态变成兑现状态,后续会进入.then 的回调函数中。
- 拒绝状态(rejected)在执行了reject后,promise状态会从待定状态变成拒绝状态,后续会进入.catch 的回调函数中。
等到Promise任务执行完毕后我们需要调用Promise对象的then方法或者catch方法来获取Promise执行的结果。
示例代码:
javascript
// 创建一个 Promise 对象
const promise = new Promise(function(resolve, reject) {
// 进行异步操作
setTimeout(() => {
// 随机生成成功或失败
if (Math.random() < 0.5) {
/*
执行resolve方法对于Promise对象来说是成功
最终执行Promise对象的then方法,传递的参数也会被传递到then方法中
*/
console.log('准备执行resolve');
resolve('success');
} else {
/*
执行reject方法对于Promise对象来说是失败
最终执行Promise对象的catch方法,传递的参数也会被传递到catch方法
*/
console.log('准备执行reject');
reject('error');
}
}, 1000);
});
// 注册成功和失败的回调函数(获取Promise对象的状态,但是是异步执行,并不阻塞下面的代码执行)
promise.then(function (result){
// result的值为resolve方法传入的值
console.log(result);
}).catch(function (error){
// error的值为reject方法传入的值
console.log(error);
}).finally(function (){
// 无论成功或失败都会执行
console.log('Promise对象执行完毕');
})
console.log('程序执行完毕'); // 因为Promise对象是异步的,所以先输出
执行效果:

需要注意的是,Promise的起始函数是同步执行的,但then/catch 方法是异步执行的。
示例代码:
javascript
// 创建一个 Promise 对象
const promise = new Promise(function (resolve, reject) {
console.log('Promise start...');
// 将来(Promise起始函数执行完毕后)会执行then方法里面的函数
resolve('成功');
// 模拟延迟
sleep(2000)
console.log('Promise end...');
});
promise.then(function (value) {
// 模拟延迟
sleep(2000)
console.log('Promise resolved:', value);
}).catch(function (reason) {
console.log('Promise rejected:', reason);
}).finally(function () {
console.log('Promise finally...');
});
console.log('程序执行完毕');
执行效果:

有了Promise,接下来我们可以改造一下回调地狱中的代码了:
javascript
function print(delay, message) {
// 该方法返回的是一个Promise对象
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(message);
resolve();
}, delay);
});
}
/*
1. print(2000, "First").then()开始执行Promise对象里面的方法
2s后打印"First",之后执行resolve方法,然后将执行then()方法
2. 在print(2000, "First").then()方法中调用了print(4000, "Second");
该方法返回一个Promise对象(返回给了print(2000, "First").then()方法)
4s后打印"Second",之后执行resolve方法,然后将执行then()方法
3. print(4000, "Second")的then()方法实际上就是print(3000, "Third");然后3s后打印"Third"
*/
print(2000, "First").then(function () {
return print(4000, "Second");
}).then(function () {
print(3000, "Third");
});
console.log("End.."); // 该代码最先执行
上述代码等价于:
javascript
let promise_first = print(2000, "First");
let promise_second = promise_first.then(function () {
return print(4000, "Second");
});
let promise_third = promise_second.then(function () {
print(3000, "Third");
});
执行效果:

5.2.3 Promise改造Ajax请求
Promise可以封装一个异步操作,不阻塞当前任务的执行,利用这个特点我们可以用来封装ajax请求,示例代码如下:
javascript
const promise = new Promise((resolve, reject) => {
// 执行异步操作(该方法在创建Promise对象时就已经执行)
if (/* 异步操作是否成功 */) {
resolve(value);// 调用 resolve,代表 Promise 将返回成功的结果(其实就是调用then方法)
} else {
reject(error);// 调用 reject,代表 Promise 会返回失败结果(其实就是调用catch方法)
}
});
示例:
javascript
let getRequest = function (url) {
// 使用Promise对象封装一个ajax请求
return new Promise((resolve, reject) => {
$.ajax({
url: url,
type: "GET",
success(result) {
// 调用Promise的resolve方法
resolve(result);
},
error(error) {
// 调用Promise的reject方法
reject(error);
}
});
})
}
// 模拟一个请求地址(较为耗时)
var url = "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json";
/*
执行getRequest(url)方法,Promise起始函数开始执行
调用then方法,获取Promise成功的结果(需要等待Promise执行完毕),并打印到控制台
调用catch方法,获取Promise失败的错误(需要等待Promise执行完毕),并打印到控制台
*/
getRequest(url).then(function (result){
console.log(result);
}).catch(function (error){
console.log(error);
})
console.log("程序执行完毕"); // 这句代码最先执行

5.2 异步函数-async/await
Promise 的编程模型依然充斥着大量的 then方法,虽然解决了回调地狱的问题,但是在语义方面依然存在缺陷,代码中充斥着大量的 then 函数,这就是 async/await 出现的原因。async/await 让代码更少,更简洁。
5.2.1 async
async 关键字修饰的方法被称为异步方法,其本质是将方法执行的内容包装为一个Promise对象。也就是说,async函数的返回值是一个Promise对象,具体的使用方法和特点我们还需要往下看。
异步函数的核心特点:
- 非阻塞执行:异步操作不会阻塞主线程
- 语法简洁:避免了 Promise 的链式调用,代码更易读
- 错误处理:可以使用传统的 try/catch 结构处理错误
异步函数的语法:
javascript
async function functionName() {
// 函数体
}
或使用箭头函数:
javascript
const funcName = async () => {
// 函数体
}
1)async函数的使用
async 函数的返回值为一个Promise对象,我们观察下列代码:
javascript
// async函数的返回值是一个Promise对象
async function timeout() {
return 'hello world!'
}
console.log(timeout())
console.log('end...')
观察结果:

当async函数正常执行完毕,那么内部会调用Promise.resolve()返回Promise对象,后续将会执行该Promise的then回调函数,并把当前的async函数的返回值传递给resolve()方法。但如果async函数内部抛出错误,那么就会调用Promise.reject()来返回Promise对象,该错误对象会被传递给reject()方法。
我们观察下列代码:
javascript
// async函数的返回值是一个Promise对象
async function timeout() {
// 如果方法没有出现异常, 则返回值会被作为resolve函数的参数
// 如果方法出现异常, 该异常对象会被作为reject函数的参数
// throw new Error('出现了异常');
return 'hello world!'
}
// timeout()方法的返回值为Promise对象
timeout().then(function (result) {
console.log("then方法:", result)
}).catch(function (error) {
console.log("catch方法:", error)
})
console.log('end...')
观察结果:

总结下来,其实async函数就是返回了一个Promise对象,当然内部会根据执行代码的不同来选择调用reslove还是reject方法来创建Promise对象,了解到本质后,我们就能理解,其实下面两个写法是等价的:
javascript
// fun1
function fun1() {
return Promise.resolve('TEST');
}
// fun2
async function fun2() {
return 'TEST';
}
2)async函数异步在哪
Promise的特点是起始函数是同步执行 ,then/catch 方法是异步执行。因而在async函数中,并非是async函数的执行是异步的,相反它是同步执行的。我们观察下面案例:
javascript
// 模拟阻塞
function sleep(delay) {
let start = new Date().getTime();
while (new Date().getTime() - start < delay) {
continue;
}
}
/*
async函数是同步执行的, 但是它可以返回一个Promise对象
async函数执行完毕后, 才会执行then方法里面的代码(then方法是异步的)
*/
async function timeout() {
// 模拟阻塞
sleep(2000)
console.log('timeout函数执行中...') // 先输出这里(因为async函数是同步执行的)
return 'hello world!'
}
// 调用timeout函数
timeout()
console.log('end...') // 再输出这里
执行效果:

可以看到,单纯的使用async函数并非能达到异步效果,而是要结合then方法来达到异步的功能。观察下列代码:
javascript
// 模拟阻塞
function sleep(delay) {
let start = new Date().getTime();
while (new Date().getTime() - start < delay) {
continue;
}
}
/*
async函数是同步执行的, 但是它可以返回一个Promise对象
async函数执行完毕后, 才会执行then方法里面的代码(then方法是异步的)
*/
async function timeout() {
// 模拟阻塞
sleep(2000)
console.log('timeout函数执行中...') // 先输出这里(因为async函数是同步执行的)
return 'hello world!'
}
// 调用timeout函数
timeout().then(function (result) {
// 模拟阻塞
sleep(2000)
console.log("then:", result) // 最后输出这里(异步执行)
})
console.log('end...') // 再输出这里
执行效果:

5.2.2 await
1)await的使用
对于传统的async函数在调用时只能返回Promise对象,而我们想要获取该Promise对象产生的结果则需要调用该Promise对象的then方法(或catch方法)来接收结果集。
await 是一个只能在async函数内部使用的运算符,加在Promise对象前面,使它会阻塞当前 async 函数的执行,等待 Promise 的完成:
- 如果 Promise fulfilled(成功),直接返回Promise Resolve方法的值,即then回调函数的参数。
- 如果 Promise rejected(失败),直接抛出拒绝原因(rejection reason)
观察案例:
javascript
// 定义一个函数,模拟请求
async function fetchData() {
console.log('fetchData...');
return 'ok';
}
async function startFetchData(){
// 返回的是Promise对象,可以继续调用then方法获取结果('ok')
let promise = fetchData()
console.log("result:",promise)
}
// 调用startFetchData函数
startFetchData();
console.log("end")
执行效果如下:

但有时我们必须获取当前Promise的结果集才能够执行后续的代码,此时就可以在Promise对象(async函数)前使用await关键字,改为如下代码:
javascript
// 定义一个函数,模拟请求
async function fetchData() {
console.log('fetchData...');
return 'ok';
}
async function startFetchData() {
/*
等待fetchData函数的返回结果,返回值为'ok'(将来要传递给then回调函数的值)
await会阻塞当前函数的代码,而不阻塞函数外的代码
*/
let result = await fetchData()
console.log("result:", result)
}
// 调用startFetchData函数
startFetchData();
console.log("end")
代码:
javascript
/*
等待fetchData函数的返回结果,返回值为'ok'(将来要传递给then回调函数的值)
await会阻塞当前函数的代码,而不阻塞函数外的代码
*/
let result = await fetchData()
console.log('result:', result);
等价于:
javascript
fetchData().then(function(result) {
console.log('result:', result);
})
执行效果如下:

2)await与async解决回调地狱
我们观察如下案例:
javascript
setTimeout(function () {
console.log("First");
setTimeout(function () {
console.log("Second");
setTimeout(function () {
console.log("Third");
}, 2000);
}, 2000);
}, 2000);
使用Promise改造:
javascript
print(2000, "First").then(function () {
return print(4000, "Second");
}).then(function () {
print(3000, "Third");
});
使用await和async改造:
javascript
// 该方法返回的是Promise对象
function print(delay, message) {
// 该方法返回的是一个Promise对象
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(message);
resolve(message + "ok");
}, delay);
});
}
// 改造后(async/await)
async function getData() {
// 获取第一个Promise的结果
const data1 = await print(2000, "First");
// 获取第二个Promise的结果(等到第一个Promise执行完毕)
const data2 = await print(2000, "Second");
// 获取第三个Promise的结果(等到第二个Promise执行完毕)
const data3 = await print(2000, "Third");
console.log('data1', data1);
console.log('data2', data2);
console.log('data3', data3);
}
// 调用改造后的getData方法
getData();