4.5. 使用Promise.race和delay取消XHR请求

在本小节中,作为在第2章所学的 Promise.race 的具体例子,我们来看一下如何使用Promise.race来实现超时机制。

当然XHR有一个 timeout 属性,使用该属性也可以简单实现超时功能,但是为了能支持多个XHR同时超时或者其他功能,我们采用了容易理解的异步方式在XHR中通过超时来实现取消正在进行中的操作。

4.5.1. 让Promise等待指定时间

首先我们来看一下如何在Promise中实现超时。

所谓超时就是要在经过一定时间后进行某些操作,使用 setTimeout 的话很好理解。

首先我们来串讲一个单纯的在Promise中调用 setTimeout 的函数。

delayPromise.js

  1. function delayPromise(ms) {
  2. return new Promise(function (resolve) {
  3. setTimeout(resolve, ms);
  4. });
  5. }

delayPromise(ms) 返回一个在经过了参数指定的毫秒数后进行onFulfilled操作的promise对象,这和直接使用 setTimeout 函数比较起来只是编码上略有不同,如下所示。

  1. setTimeout(function () {
  2. alert("已经过了100ms!");
  3. }, 100);
  4. // == 几乎同样的操作
  5. delayPromise(100).then(function () {
  6. alert("已经过了100ms!");
  7. });

在这里 promise对象 这个概念非常重要,请切记。

4.5.2. Promise.race中的超时

让我们回顾一下静态方法 Promise.race ,它的作用是在任何一个promise对象进入到确定(解决)状态后就继续进行后续处理,如下面的例子所示。

  1. var winnerPromise = new Promise(function (resolve) {
  2. setTimeout(function () {
  3. console.log('this is winner');
  4. resolve('this is winner');
  5. }, 4);
  6. });
  7. var loserPromise = new Promise(function (resolve) {
  8. setTimeout(function () {
  9. console.log('this is loser');
  10. resolve('this is loser');
  11. }, 1000);
  12. });
  13. // 第一个promise变为resolve后程序停止
  14. Promise.race([winnerPromise, loserPromise]).then(function (value) {
  15. console.log(value); // => 'this is winner'
  16. });

我们可以将刚才的 delayPromise 和其它promise对象一起放到 Promise.race 中来是实现简单的超时机制。

simple-timeout-promise.js

  1. function delayPromise(ms) {
  2. return new Promise(function (resolve) {
  3. setTimeout(resolve, ms);
  4. });
  5. }
  6. function timeoutPromise(promise, ms) {
  7. var timeout = delayPromise(ms).then(function () {
  8. throw new Error('Operation timed out after ' + ms + ' ms');
  9. });
  10. return Promise.race([promise, timeout]);
  11. }

函数 timeoutPromise(比较对象promise, ms) 接收两个参数,第一个是需要使用超时机制的promise对象,第二个参数是超时时间,它返回一个由 Promise.race 创建的相互竞争的promise对象。

之后我们就可以使用 timeoutPromise 编写下面这样的具有超时机制的代码了。

  1. function delayPromise(ms) {
  2. return new Promise(function (resolve) {
  3. setTimeout(resolve, ms);
  4. });
  5. }
  6. function timeoutPromise(promise, ms) {
  7. var timeout = delayPromise(ms).then(function () {
  8. throw new Error('Operation timed out after ' + ms + ' ms');
  9. });
  10. return Promise.race([promise, timeout]);
  11. }
  12. // 运行示例
  13. var taskPromise = new Promise(function(resolve){
  14. // 随便一些什么处理
  15. var delay = Math.random() * 2000;
  16. setTimeout(function(){
  17. resolve(delay + "ms");
  18. }, delay);
  19. });
  20. timeoutPromise(taskPromise, 1000).then(function(value){
  21. console.log("taskPromise在规定时间内结束 : " + value);
  22. }).catch(function(error){
  23. console.log("发生超时", error);
  24. });

虽然在发生超时的时候抛出了异常,但是这样的话我们就不能区分这个异常到底是普通的错误还是超时错误了。

为了能区分这个 Error 对象的类型,我们再来定义一个Error 对象的子类 TimeoutError

4.5.3. 定制Error对象

Error 对象是ECMAScript的内建(build in)对象。

但是由于stack trace等原因我们不能完美的创建一个继承自 Error 的类,不过在这里我们的目的只是为了和Error有所区别,我们将创建一个 TimeoutError 类来实现我们的目的。

在ECMAScript6中可以使用 class 语法来定义类之间的继承关系。

  1. class MyError extends Error{
  2. // 继承了Error类的对象
  3. }

为了让我们的 TimeoutError 能支持类似 error instanceof TimeoutError 的使用方法,我们还需要进行如下工作。

TimeoutError.js

  1. function copyOwnFrom(target, source) {
  2. Object.getOwnPropertyNames(source).forEach(function (propName) {
  3. Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
  4. });
  5. return target;
  6. }
  7. function TimeoutError() {
  8. var superInstance = Error.apply(null, arguments);
  9. copyOwnFrom(this, superInstance);
  10. }
  11. TimeoutError.prototype = Object.create(Error.prototype);
  12. TimeoutError.prototype.constructor = TimeoutError;

我们定义了 TimeoutError 类和构造函数,这个类继承了Error的prototype。

它的使用方法和普通的 Error 对象一样,使用 throw 语句即可,如下所示。

  1. var promise = new Promise(function(){
  2. throw TimeoutError("timeout");
  3. });
  4. promise.catch(function(error){
  5. console.log(error instanceof TimeoutError);// true
  6. });

有了这个 TimeoutError 对象,我们就能很容易区分捕获的到底是因为超时而导致的错误,还是其他原因导致的Error对象了。

本章里介绍的继承JavaScript内建对象的方法可以参考 Chapter 28. Subclassing Built-ins ,那里有详细的说明。此外 Error - JavaScript | MDN 也针对Error对象进行了详细说明。

4.5.4. 通过超时取消XHR操作

到这里,我想各位读者都已经对如何使用Promise来取消一个XHR请求都有一些思路了吧。

取消XHR操作本身的话并不难,只需要调用 XMLHttpRequest 对象的 abort() 方法就可以了。

为了能在外部调用 abort() 方法,我们先对之前本节出现的 getURL 进行简单的扩展,cancelableXHR 方法除了返回一个包装了XHR的promise对象之外,还返回了一个用于取消该XHR请求的abort方法。

delay-race-cancel.js

  1. function cancelableXHR(URL) {
  2. var req = new XMLHttpRequest();
  3. var promise = new Promise(function (resolve, reject) {
  4. req.open('GET', URL, true);
  5. req.onload = function () {
  6. if (req.status === 200) {
  7. resolve(req.responseText);
  8. } else {
  9. reject(new Error(req.statusText));
  10. }
  11. };
  12. req.onerror = function () {
  13. reject(new Error(req.statusText));
  14. };
  15. req.onabort = function () {
  16. reject(new Error('abort this request'));
  17. };
  18. req.send();
  19. });
  20. var abort = function () {
  21. // 如果request还没有结束的话就执行abort
  22. // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
  23. if (req.readyState !== XMLHttpRequest.UNSENT) {
  24. req.abort();
  25. }
  26. };
  27. return {
  28. promise: promise,
  29. abort: abort
  30. };
  31. }

在这些问题都明了之后,剩下只需要进行Promise处理的流程进行编码即可。大体的流程就像下面这样。

  1. 通过 cancelableXHR 方法取得包装了XHR的promise对象和取消该XHR请求的方法

  2. timeoutPromise 方法中通过 Promise.race 让XHR的包装promise和超时用promise进行竞争。

    • XHR在超时前返回结果的话

      1. 和正常的promise一样,通过 then 返回请求结果
    • 发生超时的时候

      1. 抛出 throw TimeoutError 异常并被 catch

      2. catch的错误对象如果是 TimeoutError 类型的话,则调用 abort 方法取消XHR请求

将上面的步骤总结一下的话,代码如下所示。

delay-race-cancel-play.js

  1. function copyOwnFrom(target, source) {
  2. Object.getOwnPropertyNames(source).forEach(function (propName) {
  3. Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
  4. });
  5. return target;
  6. }
  7. function TimeoutError() {
  8. var superInstance = Error.apply(null, arguments);
  9. copyOwnFrom(this, superInstance);
  10. }
  11. TimeoutError.prototype = Object.create(Error.prototype);
  12. TimeoutError.prototype.constructor = TimeoutError;
  13. function delayPromise(ms) {
  14. return new Promise(function (resolve) {
  15. setTimeout(resolve, ms);
  16. });
  17. }
  18. function timeoutPromise(promise, ms) {
  19. var timeout = delayPromise(ms).then(function () {
  20. return Promise.reject(new TimeoutError('Operation timed out after ' + ms + ' ms'));
  21. });
  22. return Promise.race([promise, timeout]);
  23. }
  24. function cancelableXHR(URL) {
  25. var req = new XMLHttpRequest();
  26. var promise = new Promise(function (resolve, reject) {
  27. req.open('GET', URL, true);
  28. req.onload = function () {
  29. if (req.status === 200) {
  30. resolve(req.responseText);
  31. } else {
  32. reject(new Error(req.statusText));
  33. }
  34. };
  35. req.onerror = function () {
  36. reject(new Error(req.statusText));
  37. };
  38. req.onabort = function () {
  39. reject(new Error('abort this request'));
  40. };
  41. req.send();
  42. });
  43. var abort = function () {
  44. // 如果request还没有结束的话就执行abort
  45. // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
  46. if (req.readyState !== XMLHttpRequest.UNSENT) {
  47. req.abort();
  48. }
  49. };
  50. return {
  51. promise: promise,
  52. abort: abort
  53. };
  54. }
  55. var object = cancelableXHR('http://httpbin.org/get');
  56. // main
  57. timeoutPromise(object.promise, 1000).then(function (contents) {
  58. console.log('Contents', contents);
  59. }).catch(function (error) {
  60. if (error instanceof TimeoutError) {
  61. object.abort();
  62. return console.log(error);
  63. }
  64. console.log('XHR Error :', error);
  65. });

上面的代码就通过在一定的时间内变为解决状态的promise对象实现了超时处理。

通常进行开发的情况下,由于这些逻辑会频繁使用,因此将这些代码分割保存在不同的文件应该是一个不错的选择。

4.5.5. promise和操作方法

在前面的 cancelableXHR 中,promise对象及其操作方法都是在一个对象中返回的,看起来稍微有些不太好理解。

从代码组织的角度来说一个函数只返回一个值(promise对象)是一个非常好的习惯,但是由于在外面不能访问 cancelableXHR 方法中创建的 req 变量,所以我们需要编写一个专门的函数(上面的例子中的abort)来对这些内部对象进行处理。

当然也可以考虑到对返回的promise对象进行扩展,使其支持abort方法,但是由于promise对象是对值进行抽象化的对象,如果不加限制的增加操作用的方法的话,会使整体变得非常复杂。

大家都知道一个函数做太多的工作都不认为是一个好的习惯,因此我们不会让一个函数完成所有功能,也许像下面这样对函数进行分割是一个不错的选择。

  • 返回包含XHR的promise对象

  • 接收promise对象作为参数并取消该对象中的XHR请求

将这些处理整理为一个模块的话,以后扩展起来也方便,一个函数所做的工作也会比较精炼,代码也会更容易阅读和维护。

我们有很多方法来创建一个模块(AMD,CommonJS,ES6 module etc..),在这里,我们将会把前面的 cancelableXHR 整理为一个Node.js的模块使用。

cancelableXHR.js

  1. "use strict";
  2. var requestMap = {};
  3. function createXHRPromise(URL) {
  4. var req = new XMLHttpRequest();
  5. var promise = new Promise(function (resolve, reject) {
  6. req.open('GET', URL, true);
  7. req.onreadystatechange = function () {
  8. if (req.readyState === XMLHttpRequest.DONE) {
  9. delete requestMap[URL];
  10. }
  11. };
  12. req.onload = function () {
  13. if (req.status === 200) {
  14. resolve(req.responseText);
  15. } else {
  16. reject(new Error(req.statusText));
  17. }
  18. };
  19. req.onerror = function () {
  20. reject(new Error(req.statusText));
  21. };
  22. req.onabort = function () {
  23. reject(new Error('abort this req'));
  24. };
  25. req.send();
  26. });
  27. requestMap[URL] = {
  28. promise: promise,
  29. request: req
  30. };
  31. return promise;
  32. }
  33. function abortPromise(promise) {
  34. if (typeof promise === "undefined") {
  35. return;
  36. }
  37. var request;
  38. Object.keys(requestMap).some(function (URL) {
  39. if (requestMap[URL].promise === promise) {
  40. request = requestMap[URL].request;
  41. return true;
  42. }
  43. });
  44. if (request != null && request.readyState !== XMLHttpRequest.UNSENT) {
  45. request.abort();
  46. }
  47. }
  48. module.exports.createXHRPromise = createXHRPromise;
  49. module.exports.abortPromise = abortPromise;

使用方法也非常简单,我们通过 createXHRPromise 方法得到XHR的promise对象,当想对这个XHR进行abort操作的时候,将这个promise对象传递给 abortPromise(promise) 方法就可以了。

  1. var cancelableXHR = require("./cancelableXHR");
  2. var xhrPromise = cancelableXHR.createXHRPromise('http://httpbin.org/get');(1)
  3. xhrPromise.catch(function (error) {
  4. // 调用 abort 抛出的错误
  5. });
  6. cancelableXHR.abortPromise(xhrPromise);(2)
1创建包装了XHR的promise对象
2取消在1中创建的promise对象的请求操作

4.5.6. 总结

在这里我们学到了如下内容。

  • 经过一定时间后变为解决状态的delayPromise

  • 基于delayPromise和Promise.race的超时实现方式

  • 取消XHR promise请求

  • 通过模块化实现promise对象和操作的分离

Promise能非常灵活的进行处理流程的控制,为了充分发挥它的能力,我们需要注意不要将一个函数写的过于庞大冗长,而是应该将其分割成更小更简单的处理,并对之前JavaScript中提到的机制进行更深入的了解。