06-Promise的链式调用

Promise 的链式调用:处理多次 Ajax 请求【重要】

实际开发中,我们经常需要同时请求多个接口。比如说:在请求完接口1的数据data1之后,需要根据data1的数据,继续请求接口 2,获取data2;然后根据data2的数据,继续请求接口 3。

换而言之,现在有三个网络请求,请求 2 必须依赖请求 1 的结果,请求 3 必须依赖请求 2 的结果,如果按照往常的写法,会有三层回调,会陷入“回调地狱”。

这种场景其实就是接口的多层嵌套调用。有了 Promise 之后,我们可以把多层嵌套调用按照线性的方式进行书写,非常优雅。也就是说:Promise 可以把原本的多层嵌套写法改进为链式写法

ES5 中的传统写法

  1. // 封装 ajax 请求:传入回调函数 success 和 fail
  2. function ajax(url, success, fail) {
  3. var xmlhttp = new XMLHttpRequest();
  4. xmlhttp.open('GET', url);
  5. xmlhttp.send();
  6. xmlhttp.onreadystatechange = function () {
  7. if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
  8. success && success(xmlhttp.responseText);
  9. } else {
  10. fail && fail(new Error('接口请求失败'));
  11. }
  12. };
  13. }
  14. // 执行 ajax 请求
  15. ajax(
  16. '/a.json',
  17. (res) => {
  18. console.log('qianguyihao 第一个接口请求成功:' + JSON.stringify(res));
  19. // ajax嵌套调用
  20. ajax('b.json', (res) => {
  21. console.log('qianguyihao 第二个接口请求成功:' + JSON.stringify(res));
  22. // ajax嵌套调用
  23. ajax('c.json', (res) => {
  24. console.log('qianguyihao 第三个接口请求成功:' + JSON.stringify(res));
  25. });
  26. });
  27. },
  28. (err) => {
  29. console.log('qianguyihao 请求失败:' + JSON.stringify(err));
  30. }
  31. );

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

Promise 链式调用(初步写法,方便理解)

如果我们不对 Promise 的链式调用进行封装,那么,它的简单写法是下面这样的:

  1. // 封装 ajax 请求:传入回调函数 success 和 fail
  2. function ajax(url, success, fail) {
  3. var xmlhttp = new XMLHttpRequest();
  4. xmlhttp.open('GET', url);
  5. xmlhttp.send();
  6. xmlhttp.onreadystatechange = function () {
  7. if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
  8. success && success(xmlhttp.responseText);
  9. } else {
  10. fail && fail(new Error('接口请求失败'));
  11. }
  12. };
  13. }
  14. new Promise((resolve, reject) => {
  15. ajax('a.json', (res) => {
  16. console.log(res);
  17. resolve();
  18. });
  19. })
  20. .then((res) => {
  21. console.log('a成功');
  22. return new Promise((resolve, reject) => {
  23. ajax('b.json', (res) => {
  24. console.log(res);
  25. resolve();
  26. });
  27. });
  28. })
  29. .then((res) => {
  30. console.log('b成功');
  31. return new Promise((resolve, reject) => {
  32. ajax('c.json', (res) => {
  33. console.log(res);
  34. resolve();
  35. });
  36. });
  37. })
  38. .then((res) => {
  39. cnosole.log('c成功');
  40. });

上面代码中,then 是可以链式调用的,一旦 return 一个新的 promise 实例之后,后面的 then 就可以拿到前面 resolve 出来的数据。这种扁平化的写法,更方便维护;并且可以更好的管理请求成功和失败的状态。

但是,你可能会奇怪,上面的代码,怎么这么多?而且有不少重复。因为这里只是采用了一种笨拙的方式来写,为的是方便大家理解 promise 的执行过程。我们其实可以对 promise 的链式调用进行封装。

怎么个封装法呢?上面的代码中,每次在 return 一个 promise 的时候,只是 url 地址不一样,其他的代码是一样的。所以我们可以把重复的代码封装成函数。写法如下。

Promise 链式调用(封装一个接口)

针对同一个接口的多次嵌套调用,采用 promise 封装后的写法如下:

  1. // 定义 ajax 请求:传入回调函数 success 和 fail
  2. function ajax(url, success, fail) {
  3. var xmlhttp = new XMLHttpRequest();
  4. xmlhttp.open('GET', url);
  5. xmlhttp.send();
  6. xmlhttp.onreadystatechange = function () {
  7. if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
  8. success && success(xmlhttp.responseText);
  9. } else {
  10. fail && fail(new Error('接口请求失败'));
  11. }
  12. };
  13. }
  14. // 第一步:model层,接口封装
  15. function getPromise(url) {
  16. return new Promise((resolve, reject) => {
  17. ajax(url, (res) => {
  18. // 这里的 res 是接口的返回结果。返回码 retCode 是动态数据。
  19. if (res.retCode == 0) {
  20. // 接口请求成功时调用
  21. resolve('request success' + res);
  22. } else {
  23. // 接口请求异常时调用
  24. reject({ retCode: -1, msg: 'network error' });
  25. }
  26. });
  27. });
  28. }
  29. // 第二步:业务层的接口调用。这里的 data 就是 从 resolve 和 reject 传过来的,也就是从接口拿到的数据
  30. getPromise('a.json')
  31. .then((res) => {
  32. // a 请求成功。从 resolve 获取正常结果:接口请求成功后,打印a接口的返回结果
  33. console.log(res);
  34. return getPromise('b.json'); // 继续请求 b
  35. })
  36. .then((res) => {
  37. // b 请求成功
  38. console.log(res);
  39. return getPromise('c.json'); // 继续请求 c
  40. })
  41. .then((res) => {
  42. // c 请求成功
  43. console.log(res);
  44. })
  45. .catch((e) => {
  46. // 从 reject中获取异常结果
  47. console.log(e);
  48. });

怎么样?上面代码是不是非常简洁?而且可读性很强。

代码写到这里,我们还可以再继续优化一下。细心的你可以发现,我们在做三次嵌套请求的时候,针对 resolve 和 reject 的处理时机是一样的。如果你的业务是针对同一个接口连续做了三次调用,只是请求传参不同,那么,按上面这样写是没有问题的。

但是,真正在实战中,我们往往需要嵌套请求多个不同的接口,要处理的 resolve 和 reject 的时机和逻辑往往是不同的,所以需要分开封装不同的 Promise 实例,这在实战开发中更为常见。代码应该是像下面这样写。

Promise 链式调用(封装多个接口)

针对多个不同接口的嵌套调用,采用 promise 封装后的写法如下:

  1. // 封装 ajax 请求:传入回调函数 success 和 fail
  2. function ajax(url, success, fail) {
  3. var xmlhttp = new XMLHttpRequest();
  4. xmlhttp.open('GET', url);
  5. xmlhttp.send();
  6. xmlhttp.onreadystatechange = function () {
  7. if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
  8. success && success(xmlhttp.responseText);
  9. } else {
  10. fail && fail(new Error('接口请求失败'));
  11. }
  12. };
  13. }
  14. // Promise 封装接口1
  15. function request1() {
  16. return new Promise((resolve, reject) => {
  17. ajax('https://www.baidu.com', (res) => {
  18. if (res.retCode == 201) {
  19. // 接口请求成功时调用:这里的 res 是接口1的返回结果
  20. resolve('request1 success' + res);
  21. } else {
  22. // 接口请求异常时调用异常
  23. reject('接口1请求失败');
  24. }
  25. });
  26. });
  27. }
  28. // Promise 封装接口2
  29. function request2() {
  30. return new Promise((resolve, reject) => {
  31. ajax('https://www.jd.com', (res) => {
  32. if (res.retCode == 202) {
  33. // 这里的 res 是接口2的返回结果
  34. resolve('request2 success' + res);
  35. } else {
  36. reject('接口2请求失败');
  37. }
  38. });
  39. });
  40. }
  41. // Promise 封装接口3
  42. function request3() {
  43. return new Promise((resolve, reject) => {
  44. ajax('https://www.taobao.com', (res) => {
  45. if (res.retCode == 203) {
  46. // 这里的 res 是接口3的返回结果
  47. resolve('request3 success' + res);
  48. } else {
  49. reject('接口3请求失败');
  50. }
  51. });
  52. });
  53. }
  54. // 先发起request1,等resolve后再发起request2;紧接着,等 request2有了 resolve之后,再发起 request3
  55. request1()
  56. .then((res1) => {
  57. // 接口1请求成功
  58. console.log(res1);
  59. return request2();
  60. })
  61. .then((res2) => {
  62. // 接口2请求成功
  63. console.log(res2);
  64. return request3();
  65. })
  66. .then((res3) => {
  67. // 接口3请求成功
  68. console.log(res3);
  69. })
  70. .catch((err) => {
  71. // 从 reject中获取异常结果
  72. console.log(err);
  73. });

这段代码很经典,你一定要多看几遍,多默写几遍。倒背如流也不过分。

Promise 链式调用:封装 Node.js 的回调方法

传统写法

  1. fs.readFile(A, 'utf-8', function (err, data) {
  2. fs.readFile(B, 'utf-8', function (err, data) {
  3. fs.readFile(C, 'utf-8', function (err, data) {
  4. fs.readFile(D, 'utf-8', function (err, data) {
  5. console.log('qianguyihao:' + data);
  6. });
  7. });
  8. });
  9. });

上方代码多层嵌套,存在回调地狱的问题。

Promise 写法

  1. function read(url) {
  2. return new Promise((resolve, reject) => {
  3. fs.readFile(url, 'utf8', (err, data) => {
  4. if (err) reject(err);
  5. resolve(data);
  6. });
  7. });
  8. }
  9. read(A)
  10. .then((data) => {
  11. return read(B);
  12. })
  13. .then((data) => {
  14. return read(C);
  15. })
  16. .then((data) => {
  17. return read(D);
  18. })
  19. .then((data) => {
  20. console.log('qianguyihao:' + data);
  21. })
  22. .catch((err) => {
  23. console.log(err);
  24. });

这一段代码可以看出,Promise 很好的处理了回调地狱的问题。下一篇文章,我们会更详细的介绍 Promise 的链式调用。

链式调用,如何处理 reject 失败状态

例 1:不处理 reject

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

上面的代码中,假设 a 请求失败,那么,后面的代码会怎么走呢?

打印结果:

  1. a: err
  2. undefined
  3. csuccess

我们可以看到,虽然 a 请求失败,但后续的请求依然会继续执行。

为何打印结果的第二行是 undefined?这是因为,当 a 请求走到 reject 之后,我们并没有做任何处理。这就导致,代码走到第二个 then的时候,其实是在执行一个空的 promise

例 2:单独处理 reject

  1. getPromise('a.json')
  2. .then(
  3. (res) => {
  4. console.log(res);
  5. return getPromise('b.json'); // 继续请求 b
  6. },
  7. (err) => {
  8. // a 请求失败
  9. console.log('a: err');
  10. // 【重要】即使 a 请求失败,也依然继续执行 b请求
  11. return getPromise('b.json');
  12. }
  13. )
  14. .then((res) => {
  15. // b 请求成功
  16. console.log(res);
  17. return getPromise('c.json'); // 继续请求 c
  18. })
  19. .then((res) => {
  20. // c 请求成功
  21. console.log('c:success');
  22. });

跟例 1 相比,例 2 在 reject 中增加了一行return getPromise('b.json'),意味着,即使 a 请求失败,也要继续执行 b。

这段代码,我们是单独处理了 a 请求失败的情况。

统一处理 reject

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

代码举例如下:

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

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

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

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

return 的返回值

return 后面的返回值,有两种情况:

  • 情况 1:返回 Promise 实例对象。返回的该实例对象会调用下一个 then。

  • 情况 2:返回普通值。返回的普通值会直接传递给下一个 then,通过 then 参数中函数的参数接收该值。

我们针对上面这两种情况,详细解释一下。

情况 1:返回 Promise 实例对象

举例如下:

  1. getPromise('a.json')
  2. .then((res) => {
  3. // a 请求成功。从 resolve 获取正常结果:接口请求成功后,打印a接口的返回结果
  4. console.log(res);
  5. // 这里的 return,返回的是 Promise 实例对象
  6. return new Promise((resolve, reject) => {
  7. resolve('qianguyihao');
  8. });
  9. })
  10. .then((res) => {
  11. console.log(res);
  12. });

情况 2:返回 普通值

  1. getPromise('a.json')
  2. .then((res) => {
  3. // a 请求成功。从 resolve 获取正常结果:接口请求成功后,打印a接口的返回结果
  4. console.log(res);
  5. // 返回普通值
  6. return 'qianguyihao';
  7. })
  8. /*
  9. 既然上方代码并没有返回 promise,那么,这里的 then 是谁来调用呢?
  10. 答案是:这里会产生一个新的 默认的 promise实例,来调用这里的then,确保可以继续进行链式操作。
  11. */
  12. .then((res2) => {
  13. // 这里的 res2 接收的是 普通值 'qianguyihao'
  14. console.log(res2);
  15. });

我的公众号

想学习更多技能?不妨关注我的微信公众号:千古壹号

扫一扫,你将发现另一个全新的世界,而这将是一场美丽的意外:

06-Promise的链式调用 - 图1