Javascript 笔记:Promise

Promise 提供了一种异步编程的模式,大大简化化异步编程的难度。

这篇文章主要是记录了它的基本概念和使用方式。

基本概念

Promise 是异步编程的一种新的解决方案和规范。ES6将其写进了语言标准,统一了用法,原生提供了 Promise 对象。

Promise 对象, 可以用同步的表现形式来书写异步代码(也就是说,代码看起来是同步的,但本质上的运行过程是异步的)。使用 Promise 主要有以下优点:

  1. 可以很好地解决ES5中的回调地狱的问题(避免了层层嵌套的回调函数)。
  2. 统一规范、语法简洁、可读性和和可维护性强。
  3. Promise 对象提供了简洁的 API,使得管理异步任务更方便、更灵活。

从语法上讲,Promise 是一个构造函数。从功能上来说,Promise 对象用于封装一个异步操作,并获取其成功/ 失败的结果值。

使用 Promise 处理异步任务的基本代码结构如下,我们先来认识一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 使用 Promise 处理异步任务的基本模型

// 封装异步任务
function requestData(url) {

// resolve 和 reject 这两个单词是形参,可以自由命名。大家的习惯写法是写成 resolve 和 reject
const promise = new Promise((resolve, reject) => {
const res = {
retCode: 0,
data: 'qiangu yihao`s data',
errMsg: 'not login',
};
setTimeout(() => {
if (res.retCode == 0) {
// 网络请求成功
resolve(res.data);
} else {
// 网络请求失败
reject(res.errMsg);
}
}, 1000);
});
return promise;
}


// 调用异步任务
requestData('www.qianguyihao.com/index1').then(data => {
console.log('异步任务执行成功:', data);
}).catch(err=> {
console.log('异步任务执行失败:', err);
})

// 再次调用异步任务
requestData('www.qianguyihao.com/index2').then(data => {
console.log('异步任务再次执行成功:', data);
}).catch(err=> {
console.log('异步任务再次执行失败:', err);
})

Promise 处理异步的基本流程如上所示,可以简化为如下的结构:

1
2
3
4
5
6
7
8
const promise = new Promise(executor);

// 【划重点】下面这两行代码是等价的,选其中一种写法即可。这两种写法没有区别,只是写法形式上的区别
// 写法一
promise.then(onFulfilled, onRejected);

// 写法二
promise.then(onFulfilled).catch(onRejected);

Promise 是一个类,通过 new Promise() 进行实例化,构造出一个 Promise 实例对象。

  1. Promise 的构造函数中需要传入一个参数,这个参数是一个回调函数,常用于处理异步任务。这个回调函数有一个专有名词叫 executor(执行器),因为在 new Promise() 时,这个函数会立即执行。 可以在该回调函数中传入两个参数:resolve 和 reject。我们可以在适当的时机执行 resolve()、reject(),用于改变当前 Promise 实例的状态为成功或失败,其中成功的结果或失败的结果通过参数传入到 resolve()、reject()中,如 resolve(成功的结果) 或 reject(失败的结果),最终将结果传递给 then() 方法的成功回调函数或失败回调函数。
    • 当 Promise 状态变为成功时,会触发 then() 方法里的回调函数的执行,对成功的返回结果进行处理。
    • 当 Promise 状态变为失败时,会触发 catch() 方法里的回调函数的执行,,对失败的返回结果进行处理。
  2. then()方法的括号里面有两个参数,分别代表两个回调函数 onFulfilled 和 onRejected,执行的结果为作为参数传递给这两个回调函数,如 onFulfilled(成功的结果) 或 onRejected(失败的结果) :
    • 参数1:成功的回调函数。如果 Promise 的状态为 fulfilled(意思是:任务执行成功),则触发 onFulfilled 函数的执行。
    • 参数2:失败的回调函数。如果 Promise 的状态为 rejected(意思是,任务执行失败),则触发 onRejected 函数的执行。
  3. 只有 Promise 的状态被改变之后,才会走到 then() 或者 catch()。也就是说,在 new Promise() 时,如果没有写 resolve(),则 promise.then() 不执行;如果没有写 reject(),则 promise.catch() 不执行。
  4. resolve() 和 reject() 这两个方法,可以给 promise.then()、promise.catch() 传递参数。
  5. then() 可以被多次调用,会按照顺序执行。

回调函数 onFulfilled 和 onRejected 的执行依赖 Promise 状态的改变,我们可以将 Promise 划分为三种状态:

  • pending:等待中。属于初始状态,既没有被兑现,也没有被拒绝。
  • fulfilled:已兑现/已解决/成功。执行了resolve() 时,立即处于该状态,表示 Promise已经被解决,任务执行成功,触发 onFulfilled 函数。
  • rejected:已拒绝/失败。执行了 reject()时,立即处于该状态,表示 Promise已经被拒绝,任务执行失败,触发 onRejected 函数。

其状态转换如下所示: Js promise status

封装异步任务

在使用 Promise 之前,异步任务一般使用回调函数来实现,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 封装 ajax 请求:传入回调函数 success 和 fail
function ajax(url, success, fail) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', url);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
success && success(xmlhttp.responseText);
} else {
// 这里的 && 符号,意思是:如果传了 fail 参数,就调用后面的 fail();如果没传 fail 参数,就不调用后面的内容。因为 fail 参数不一定会传。
fail && fail(new Error('接口请求失败'));
}
};
}

// 执行 ajax 请求
ajax(
'/a.json',
(res) => {
console.log('qianguyihao 第一个接口请求成功:' + JSON.stringify(res));
},
(err) => {
console.log('qianguyihao 请求失败:' + JSON.stringify(err));
}
);

在上面的代码中,定义和执行 ajax 时需要传⼊ success 和 fail 这两个回调函数,进而执行回调函数。

下面可以使用 Promise 封装这个异步任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 封装 ajax 请求:传入回调函数 success 和 fail
function ajax(url, success, fail) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', url);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
success && success(xmlhttp.responseText);
} else {
// 这里的 && 符号,意思是:如果传了 fail 参数,就调用后面的 fail();如果没传 fail 参数,就不调用后面的内容。因为 fail 参数不一定会传。
fail && fail(new Error('接口请求失败'));
}
};
}

// 第一步:model层的接口封装
const promiseB = new Promise((resolve, reject) => {
ajax('xxx_a.json', (res) => {
// 这里的 res 是接口的返回结果。返回码 retCode 是动态数据。
if (res.retCode == 0) {
// 接口请求成功时调用
resolve('request success' + res);
} else {
// 接口请求失败时调用
reject({ retCode: -1, msg: 'network error' });
}
});
});

// 第二步:业务层的接口调用。这里的 data 就是 从 resolve 和 reject 传过来的,也就是从接口拿到的数据
promiseB
.then((res) => {
// 从 resolve 获取正常结果
console.log(res);
})
.catch((err) => {
// 从 reject 获取异常结果
console.log(err);
});

上方代码中,当从接口返回的数据 data.retCode 的值(接口返回码)不同时,可能会走 resolve,也可能会走 reject,这个由你自己的业务决定。

接口返回的数据,一般是 { retCode: 0, msg: 'qianguyihao' } 这种 json 格式, retCode 为 0 代表请求接口成功,所以前端对应会写 if (res.retCode == 0) 这样的逻辑。

resolve() 传入的参数

执行 resolve()之后,Promise 的状态一定会变成 fulfilled 吗?这是不一定的。

严格来说,在我们调用 resolve 时,如果 resolve()的参数中传入的值本身不是一个Promise,那么会将该 promise 的状态变成 fulfilled。

resolve()的参数中,可以传入哪些值,Promise会进入哪种状态呢?具体情况如下:

  • 情况1:如果resolve()中传入普通的值或者普通对象(包括 undefined),那么 Promise 的状态为fulfilled。这个值会作为then()回调的参数。这是最常见的情况。
  • 情况2:如果resolve()中传入的是另外一个新的 Promise,那么原 Promise 的状态将交给新的 Promise 决定。
  • 情况3:如果resolve()中传入的是一个对象,并且这个对象里有实现then()方法(这种对象称为 thenable 对象),那就会执行该then()方法,并且根据then()方法的结果来决定Promise的状态

情况3中,我们通常称这个对象为 thenable 对象。thenable 的意思是,在某个对象或者函数中定义了一个 then() 方法,我们就称其为 thenable 对象/thenable函数。注意,thenable对象里面的那个单词只能写 then,不能写其他的单词;如果写其他的单词,就不是 thenable 对象了,就不符合情况3,而是符合情况1。

Promis 方法

then 方法

then() 方法可以被多次调用,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const myPromise = new Promise((resolve, reject) => {
resolve('qianguyihao');
});

myPromise.then(res => {
console.log('成功回调1');
console.log('res1:', res);
});

myPromise.then(res => {
console.log('成功回调2');
console.log('res2:', res);
});

myPromise.then(res => {
console.log('成功回调3');
console.log('res3', res);
});

打印结果:

1
2
3
4
5
6
7
8
成功回调1
res1: qianguyihao

成功回调2
res2: qianguyihao

成功回调3
res3 qianguyihao

代码解释: 当 myPromise 状态为 fulfilled 时,下面的四个 then() 方法都在监听,所以这四个 then() 方法都会收到状态确定的通知,进而都会执行。

此外,then() 被调用多次还有一种链式调用的写法,它的打印结果与上面的打印结果不同,想要了解 Promise 的链式调用,需要先学习 then() 方法的返回值。

then 方法的返回值

then() 方法本身是有返回值的,它会返回一个新的Promise对象。因为 then()方法的返回值永远是一个 Promise 对象,所以我们才可以对它进行链式调用。

Promise 链式调用的伪代码:

1
2
// 伪代码
myPromise.then().then().catch()

那么,then()方法返回的 Promise 对象处于什么状态呢?then()方法的参数里,是一个回调函数。这取决于回调函数的返回值是什么。情况如下:

  1. 当then()方法中的回调函数在执行时,那么Promise 处于pending状态。

  2. 当 then()方法中的回调函数中,手动 return 一个返回值时,那么 Promise 的状态取决于返回值的类型。当返回值这行代码执行完毕后, Promise 会立即决议,进入确定状态(成功 or 失败)。具体情况如下:

    • 情况1:如果没有返回值(相当于 return undefined),或者返回值是普通值/普通对象,那么 Promise 的状态为 fulfilled。这个值会作为fulfilled 状态的回调函数的参数值。
    • 情况2:如果返回值是另外一个新的 Promise,那么原 Promise(then 方法返回的默认 promise)的状态将交给新的 Promise 决定,这两个 Promise 的状态一致。
    • 情况3:如果返回值是一个对象,并且这个对象里有实现 then()方法(这种对象称为 thenable 对象),那就会执行该 then()方法,并且根据then() 方法的结果来决定P romise的状态
    • 情况4:这是一种特殊情况,当 then() 方法传入的回调函数遇到异常或者手动抛出异常时,那么, Promise 处于rejected 状态,并将抛出的错误作为 rejected 状态的回调函数的参数值。

小结: then()方法里,我们可以通过 return 传递结果和状态给下一个新的 Promise。

默认返回值

如果then()方法的回调函数里没写返回值(相当于 return undefined),那么then()方法的返回值是一个新的Promise。新 Promise 的状态为fulfilled,其then()方法里,res的值为 undefined。

then() 链式调用的代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const myPromise = new Promise((resolve, reject) => {
resolve('qianguyihao');
});

myPromise
.then(res => {
console.log('成功回调1');
console.log('res1:', res);
/*
这里虽然什么都没写,底层默认写了如下代码:
return new Promise((resolve, reject) => {
resolve(); // resolve() 的参数是空,相当于 resolve(undefined)
})
*/
})
.then(res => {
console.log('成功回调2');
console.log('res2:', res);
})
.then(res => {
console.log('成功回调3');
console.log('res3', res);
});

打印结果:

1
2
3
4
5
6
7
8
成功回调1
res1: qianguyihao

成功回调2
res2: undefined

成功回调3
res3:undefined

代码解释: 第一个 then()里的回调,是由 myPromise 进行决议。第二个then()、第三个then() 也在等待决议

但是,第二个 then() 的回调是由第一个 then()传入的回调函数,返回的 Promise 进行决议;第三个 then() 的回调是由第二个 then()传入的回调函数,返回的 Promise 进行决议,以此类推。所以,这两个then()里面的打印参数的结果是 undefined,并没有打印 myPromise 的决议结果。

换句话说,第一个 then() 在等待 myPromise 的决议结果,有决议结果后执行;第二个 then() 在等待第一个 then()参数里返回的新 Promise的决议结果,有决议结果后执行;第三个 then() 在等待第二个 then()参数里返回的新 Promise的决议结果,有决议结果后执行。

返回普通值:通过 return 传递数据结果

我们也可以在 then()方法的回调函数里,手动 return 自己想要的数据,比如一个普通值 value1。这个普通值就可以传递给下一个新的Promise。新 Promise 的状态为fulfilled,其then()方法里,res的值为 value1。

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const myPromise = new Promise((resolve, reject) => {
resolve('1号');
});

myPromise
.then(res => {
console.log('res1:', res);
// return一个普通值,把这个值传递给下一个Promise
return '2号';
/*
上面这行 return,相当于:
return new Promise((resolve, reject)=> {
resolve('2号');
})
*/
})
.then(res => {
// res可以接收到上一个 Promise 传递的值
console.log('res2:', res);
})
.then(res => {
console.log('res3:', res);
});

返回结果:

1
2
3
res1: 1号
res2: 2号
res3: undefined

返回新的 Promise

情况1、在 then() 方法的回调函数中 return 一个成功的新 Promise,那么,then()返回的Promise 也是成功状态。相当于把新Promise的成功结果传递出去。代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const promise1 = new Promise((resolve, reject) => {
resolve('qianguyihao fulfilled 1');
});

const promise2 = new Promise((resolve, reject) => {
resolve('qianguyihao fulfilled 2');
});

promise1
.then(res => {
console.log('res1:', res);
return promise2;
})
.then(res => {
// 监听 promise2 的成功状态
console.log('res2:', res);
})
.then(res => {
console.log('res3', res);
});

打印结果:

1
2
3
res1: qianguyihao fulfilled 1
res2: qianguyihao fulfilled 2
res3 undefined

情况2、在 then() 方法的回调函数中 return 一个失败的新 Promise,那么,then()返回的Promise 也是失败状态。再继续往下走,会怎么样?相当于把新Promise 的失败原因传递出去。代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const promise1 = new Promise((resolve, reject) => {
resolve('qianguyihao fulfilled 1');
});

const promise2 = new Promise((resolve, reject) => {
reject('qianguyihao rejected 2');
});

promise1
.then(res => {
console.log('res1:', res);
// return 一个 失败的 Promise
return promise2;
})
.then(res => {
console.log('res2:', res);
}, err => {
// 如果 promise2 为失败状态,可以通过 then() 的第二个参数(即失败的回调函数)捕获异常,然后就可以继续往下执行其他的代码
console.log('err2:', err);
// 这里相当于 return undefined
})
.then(res => {
console.log('res3', res);
}, err => {
console.log('err3:', err);
});
打印结果:
1
2
3
res1: qianguyihao fulfilled 1
err2: qianguyihao rejected 2
res3: undefined

上方代码可以看到,第二个 Promise 走的是失败回调,这很容易理解。重点是,最后一个 Promise 走的是成功回调,这很出人意料。这主要是因为第二个失败的回调捕获了异常,并返回了一个 undefined 的值,具体的含义见下文的解释。

catch 方法

一个 Promise 的 catch() 方法可以被多次调用。每次调用时我们都可以传入对应 rejected 状态的回调函数。当 Promise 的状态变为 rejected 时,这些回调函数都会被执行。

catch() 被调用多次的伪代码:

1
2
3
4
5
const myPromise = new Promise();

myPromise.catch();
myPromise.catch();
myPromise.catch();

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const myPromise = new Promise((resolve, reject) => {
reject('qianguyihao rejected');
});

myPromise.catch(err => {
console.log('失败回调1');
console.log('err1:', err);
});

myPromise.catch(err => {
console.log('失败回调2');
console.log('err2:', err);
});

myPromise.catch(err => {
console.log('失败回调3');
console.log('err3:', err);
});

打印结果:

1
2
3
4
5
6
7
8
失败回调1
err1: qianguyihao rejected

失败回调2
err2: qianguyihao rejected

失败回调3
err3: qianguyihao rejected

代码解释:

当 myPromise 状态为 rejected 时,下面的四个 catch() 方法都在监听,所以这四个 catch() 方法都会收到状态确定的通知,进而都会执行。

catch 方法的返回值

与 then() 方法类似,catch()方法默认也是有返回值的,它会返回一个新的Promise对象。因为 catch()方法的返回值永远是一个 Promise 对象,所以我们才可以对它进行链式调用。

Promise 链式调用的伪代码:

1
2
// 伪代码
myPromise.then().then().catch().then()

上方代码中,因为 myPromise.catch() 的返回值本身就是一个 Promise,所以才可以继续调用 then()、继续调用 catch()。

与 then() 方法类似,catch()方法返回的 Promise 对象处于什么状态呢?catch()方法的参数里,是一个回调函数。这取决于回调函数的返回值是什么。情况如下:

  1. 当catch()方法中的回调函数在执行时,那么Promise 处于 pending 状态。

  2. 当 catch方法中的回调函数中,手动 return 一个返回值时,那么 Promise 的状态取决于返回值的类型。当返回值这行代码执行完毕后, Promise 会立即决议,进入确定状态(成功 or 失败),进而触发下一个then/catch 函数的执行。同时可以给下一个 then/catch 传递参数。具体情况如下:

    • 情况1:如果没有返回值(相当于 return undefined),或者返回值是普通值/普通对象,那么 Promise 的状态为fulfilled。这个值会作为then()回调的参数。
    • 情况2:如果返回值是另外一个新的 Promise,那么原 Promise(then 方法返回的默认 promise) 的状态将交给新的 Promise 决定。这两个Promise 的状态一致。
    • 情况3:如果返回值是一个对象,并且这个对象里有实现then()方法(这种对象称为 thenable 对象),那就会执行该then()方法,并且根据then()方法的结果来决定Promise的状态。
    • 情况4:这是一种特殊情况, 当catch()方法传入的回调函数遇到异常或者手动抛出异常时,那么, Promise 处于rejected 状态。

小结: catch()方法里,我们可以通过 return 传递结果 给下一个新的 Promise。

默认返回值

如果 catch() 方法的回调函数里没写返回值(相当于 return undefined),那么 catch() 方法的返回值是一个新的Promise。新 Promise 的状态为fulfilled,其then()方法里,res的值为 undefined。

返回普通值

我们也可以在 catch()方法的回调函数里,手动 return 自己想要的数据,比如一个普通值 value1。这个普通值就可以传递给下一个新的Promise。新 Promise 的状态为fulfilled,其then()方法里,res的值为 value1。 代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const myPromise = new Promise((resolve, reject) => {
reject('1号');
});

myPromise
.catch(err => {
console.log('err1:', err);
return '2号';
/*
上面这行 return,相当于:
return new Promise((resolve, reject)=> {
resolve('2号');
})
*/
})
.then(res => {
console.log('res2:', res);
})
.then(res => {
console.log('res3:', res);
});

返回结果:

1
2
3
err1: 1号
res2: 2号
res3: undefined

catch 方法的执行时机

Promise 抛出 rejected 异常时,一定要捕获并处理,否则会直接报错。在 Promise 中,发生异常时,它会它会找到最近的那个失败回调函数并执行;

找到最近的 catch() 去执行

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const myPromise = new Promise((resolve, reject) => {
reject('qianguyihao rejected');
});

myPromise
.then(res => {
console.log('res1:', res);
})
.then(res => {
console.log('res2:', res);
})
.catch(err => {
console.log('err:', err);
});

打印结果:

1
err: qianguyihao rejected

上方代码中的 catch() 是属于哪个 Promise 实例的方法呢?其实没有严格的界限。它既可以捕获 myPromise的异常,也可以捕获那两个 then()的异常。

处理失败状态的两种写法

我们有两种写法可以捕获 Promise的失败/异常状态:

  • 写法 1:单独写 catch() 方法作为失败的回调函数。
  • 写法 2:then()方法里可以传两个参数,第⼀个参数是成功时的回调函数,第⼆个参数是失败时的回调函数。

这两种写法在实战开发中的代码举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function myPromise() {
return new Promise((resolve, reject) => {
// 这里做异步任务(比如 ajax 请求接口,或者定时器)
...
...
});
}

// 写法1
myPromise()
.then((res) => {
// 从 resolve 获取正常结果
console.log('接口请求成功时,走这里');
console.log(res);
})
.catch((err) => {
// 从 reject 获取异常结果
console.log('接口请求失败时,走这里');
console.log(err);
})
.finally(() => {
console.log('无论接口请求成功与否,都会走这里');
});


// 写法 2:(和写法 1 等价)
myPromise()
.then(
(res) => {
// 从 resolve 获取正常结果
console.log('接口请求成功时,走这里');
console.log(res);
},
(err) => {
// 从 reject 获取异常结果
console.log('接口请求失败时,走这里');
console.log(err);
}
)
.finally(() => {
console.log('无论接口请求成功与否,都会走这里');
});

注意事项:

  1. 上面这两种写法是等价的,选其中一种写法即可。这两种写法几乎没有区别。

  2. 有一点点区别:

    • myPromise.then(onFulfilled).catch(onRejected):既可以捕获到 myPromise 的异常,也可以捕获到 then() 里面的异常
    • myPromise.then(onFulfilled, onRejected):只能捕获到 promise 的异常,无法捕获then()里面的异常。

知识拓展:myPromise.catch().then() 这种写法,只能捕获到 myPromise 里面的异常。

链式调用

实际开发中,我们经常需要先后请求多个接口:发送第一次网络请求后,等待请求结果;有结果后,然后发送第二次网络请求,等待请求结果;有结果后,然后发送第三次网络请求。以此类推。

比如说:在请求完接口 1 的数据data1之后,需要根据data1的数据,继续请求接口 2,获取data2;然后根据data2的数据,继续请求接口 3。换而言之,现在有三个网络请求,请求 2 必须依赖请求 1 的结果,请求 3 必须依赖请求 2 的结果。

如果按照往常的写法,会有三层回调,陷入“回调地狱”的麻烦。

这种场景其实就是接口的多层嵌套调用,在前端的异步编程开发中,经常遇到。有了 Promise 以及更高级的写法之后,我们可以把多层嵌套调用按照线性的方式进行书写,非常优雅。也就是说:Promise 等ES6的写法可以把原本的多层嵌套写法改进为链式写法。

传递回调函数

伪代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 封装 ajax 请求:传入请求地址、请求参数,以及回调函数 success 和 fail。
function requestAjax(url, params, success, fail) {
var xhr = new xhrRequest();
// 设置请求方法、请求地址。请求地址的格式一般是:'https://api.example.com/data?' + 'key1=value1&key2=value2'
xhr.open('GET', url);
// 设置请求头(如果需要)
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
success && success(xhr.responseText);
} else {
fail && fail(new Error('接口请求失败'));
}
};
}

// ES5的传统写法,执行 ajax 请求,层层嵌套
requestAjax(
'https://api.qianguyihao.com/url_1', params_1,
res1 => {
console.log('第一个接口请求成功:' + JSON.stringify(res1));
// ajax嵌套调用
requestAjax('https://api.qianguyihao.com/url_2', params_2, res2 => {
console.log('第二个接口请求成功:' + JSON.stringify(res2));
// ajax嵌套调用
requestAjax('https://api.qianguyihao.com/url_3', params_3, res3 => {
console.log('第三个接口请求成功:' + JSON.stringify(res3));
});
});
},
(err1) => {
console.log('qianguyihao 请求失败:' + JSON.stringify(err1));
}
);

上面的代码层层嵌套,可读性很差,而且出现了我们常说的回调地狱问题。

Promise 的嵌套写法

改用 ES6 的 Promise 之后,写法上会稍微改进一些。代码举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 【公共方法层】封装 ajax 请求的伪代码。传入请求地址、请求参数,以及回调函数 success 和 fail。
function requestAjax(url, params, success, fail) {
var xhr = new xhrRequest();
// 设置请求方法、请求地址。请求地址的格式一般是:'https://api.example.com/data?' + 'key1=value1&key2=value2'
xhr.open('GET', url);
// 设置请求头(如果需要)
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
success && success(xhr.responseText);
} else {
fail && fail(new Error('接口请求失败'));
}
};
}

// 【model层】将接口请求封装为 Promise
function requestData1(params_1) {
return new Promise((resolve, reject) => {
requestAjax('https://api.qianguyihao.com/url_1', params_1, res => {
// 这里的 res 是接口返回的数据。返回码 retCode 为 0 代表接口请求成功。
if (res.retCode == 0) {
// 接口请求成功时调用
resolve('request success' + res);
} else {
// 接口请求异常时调用
reject({ retCode: -1, msg: 'network error' });
}
});
});
}


// requestData2、requestData3的写法与 requestData1类似。他们的请求地址、请求参数、接口返回结果不同,所以需要挨个单独封装 Promise。
function requestData2(params_2) {
return new Promise((resolve, reject) => {
requestAjax('https://api.qianguyihao.com/url_2', params_2, res => {
if (res.retCode == 0) {
resolve('request success' + res);
} else {
reject({ retCode: -1, msg: 'network error' });
}
});
});
}

function requestData3(params_3) {
return new Promise((resolve, reject) => {
requestAjax('https://api.qianguyihao.com/url_3', params_3, res => {
if (res.retCode == 0) {
resolve('request success' + res);
} else {
reject({ retCode: -1, msg: 'network error' });
}
});
});
}

// 【业务层】Promise 调接口的嵌套写法。温馨提示:这段代码在接下来的学习中,会被改进无数次。
// 发送第一次网络请求
requestData1(params_1).then(res1 => {
console.log('第一个接口请求成功:' + JSON.stringify(res1));

// 发送第二次网络请求
requestData1(params_2).then(res2 => {
console.log('第二个接口请求成功:' + JSON.stringify(res2));

// 发送第三次网络请求
requestData1(params_3).then(res3 => {
console.log('第三个接口请求成功:' + JSON.stringify(res3));
})
})
})

上方代码非常经典。在真正的实战中,我们往往需要嵌套请求多个不同的接口,它们的接口请求地址、要处理的 resolve 和 reject 的时机、业务逻辑往往是不同的,所以需要分开封装不同的 Promise 实例。也就是说,如果要调三个不同的接口,建议单独封装三个不同的 Promise 实例:requestData1、requestData2、requestData3。

Promise 的链式调用写法

针对多个不同接口的嵌套调用,采用 Promise 的链式调用写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
requestData1(params_1).then(res1 => {
console.log('第一个接口请求成功:' + JSON.stringify(res1));
// 【关键代码】继续请求第二个接口。如果有需要,也可以把 res1 的数据传给 requestData2()的参数
return requestData2(res1);
}).then(res2 => {
console.log('第二个接口请求成功:' + JSON.stringify(res2));
// 【关键代码】继续请求第三个接口。如果有需要,也可以把 res2 的数据传给 requestData3()的参数
return requestData3(res2);
}).then(res3 => {
console.log('第三个接口请求成功:' + JSON.stringify(res3));
}).catch(err => {
console.log(err);
})

上面代码中,then 是可以链式调用的,一旦 return 一个新的 Promise 实例之后,后面的 then() 就可以作为这个新 Promise 在成功后的回调函数。这种扁平化的写法,更方便维护,可读性更好;并且可以更好的管理请求成功和失败的状态。

用 async ... await 封装链式调用

前面讲的 Promise 链式调用是用 then().then().then() 这种写法。其实我们还可以用更高级的写法,也就是用生成器、用 async ... await 改写那段代码。改进之后,代码写起来非常简洁。

说明:生成器是一种特殊的迭代器,async ... await 是生成器的语法糖。

用生成器封装链式调用

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 封装 Promise 链式请求
function* getData(params_1) {
// 【关键代码】
const res1 = yield requestData1(params_1);
const res2 = yield requestData2(res1);
const res3 = yield requestData3(res2);
}

// 调用 Promise 链式请求
const generator = getData(params_1);

generator.next().value.then(res1 => {
generator.next(res1).value.then(res2 => {
generator.next(res2).value.then(res3 => {
generator.next(res3);
})
})
})

生成器在执行时,是分阶段执行的,每次遇到 next() 方法后就会执行一个阶段,遇到 yield 就会结束当前阶段的执行并暂停。 上方代码中,yield 后面的内容是当前阶段产生的 Promise 对象;yield 前面的内容是要传递给下一个阶段的参数。

用 async ... await 封装链式调用

上面的生成器代码有些晦涩难懂,实际开发中,通常不会这么写。我们更喜欢用 async ... await 语法封装 Promise 的链式调用。async ... await 是属于生成器的语法糖,写起来更简洁直观、更容易理解。

代码举例:

1
2
3
4
5
6
7
8
// 封装:用 async ... await 调用 Promise 链式请求
async function getData() {
const res1 = await requestData1(params_1);
const res2 = await requestData2(res1);
const res3 = await requestData3(res2);
}

getData();

上面的代码非常简洁。实际开发中也经常用到,非常实用。

链式调用,如何处理任务失败的情况

在链式调用多个异步任务的Promise时,如果中间有一个任务失败或者异常,要怎么处理呢?是继续往下执行?还是停止执行,直接抛出异常?这取决于你的业务逻辑是怎样的。

常见的处理方案有以下几种,你可以根据具体情况按需选择。

统一处理失败的情况,不继续往下走

针对 a、b、c 这三个请求,不管哪个请求失败,我都希望做统一处理。这种代码要怎么写呢?我们可以在最后面写一个 catch。

由于是统一处理多个请求的异常,所以只要有一个请求失败了,就会马上走到 catch,剩下的请求就不会继续执行。比如说:

  • a 请求失败:然后会走到 catch,不执行 b 和 c;
  • a 请求成功,b 请求失败:然后会走到 catch,不执行 c。

代码举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
getPromise('a.json')
.then((res) => {
console.log(res);
return getPromise('b.json'); // 继续请求 b
})
.then((res) => {
// b 请求成功
console.log(res);
return getPromise('c.json'); // 继续请求 c
})
.then((res) => {
// c 请求成功
console.log('c:success');
})
.catch((err) => {
// 统一处理请求失败
console.log(err);
});

中间的任务失败后,如何继续往下走?

在多个Promise的链式调用中,如果中间的某个Promise 执行失败,还想让剩下的其他 Promise 顺利执行的话,那就请在中间那个失败的Promise里加一个失败的回调函数(可以写到then函数的第二个参数里,也可以写到catch函数里)。捕获异常后,便可继续往下执行其他的Promise。

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const promise1 = new Promise((resolve, reject) => {
resolve('qianguyihao fulfilled 1');
});

const promise2 = new Promise((resolve, reject) => {
reject('qianguyihao rejected 2');
});

const promise3 = new Promise((resolve, reject) => {
resolve('qianguyihao fulfilled 3');
});


promise1
.then(res => {
console.log('res1:', res);
// return 一个 失败的 Promise
return promise2;
})
.then(res => {
console.log('res2:', res);
return promise3;
}, err => {
// 如果 promise2 为失败状态,可以通过 then() 的第二个参数(即失败的回调函数)捕获异常,然后就可以继续往下执行其他 Promise
console.log('err2:', err);
// 关键代码:即便 promise2 失败了,也要继续执行 Promise3
return promise3;
})
.then(res => {
console.log('res3', res);
}, err => {
console.log('err3:', err);
});

打印结果:

1
2
3
res1: qianguyihao fulfilled 1
err2: qianguyihao rejected 2
res3 qianguyihao fulfilled 3

上方代码中,我们单独处理了 promise2 失败的情况。不管promise2 成功还是失败,我们都想让后续的 promise3 正常执行。

Promise 类的方法简介

Promise 类的方法:可以直接通过大写的Promise.xxx调用的方法。这里的xxx就称之为静态方法。

Promise 的自带 API 提供了如下静态方法:

Promise 的静态方法 含义 版本
Promise.resolve() 返回一个成功状态的 Promise 对象 ES 2015
Promise.reject() 返回一个失败状态的 Promise 对象 ES 2015
Promsie.all() 所有 Promise 都执行成功才算成功;或者任意一个 Promise 执行失败,就算失败 ES 2015
Proimse.allSettled() 不论成功与失败,把所有Promise的执行结果全部返回 ES 2020
Promise.race() Promise集合中,返回第一个执行完成(无论成功与失败)的 Promise ES 2015
Promise.any() Promise集合中,返回第一个执行成功的Promise ES 2021


参考:


1. Promise入门详解

2. Promise实例的方法

3. Promise的链式调用