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

最后更新于:2022-04-01 21:11:11

在本小节中,作为在第[2章](http://liubin.github.io/promises-book/#ch2-promise-race)所学的 [`Promise.race`](http://liubin.github.io/promises-book/#Promise.race) 的具体例子,我们来看一下如何使用Promise.race来实现超时机制。 当然XHR有一个 [timeout](https://developer.mozilla.org/ja/docs/XMLHttpRequest/Synchronous_and_Asynchronous_Requests) 属性,使用该属性也可以简单实现超时功能,但是为了能支持多个XHR同时超时或者其他功能,我们采用了容易理解的异步方式在XHR中通过超时来实现取消正在进行中的操作。 ## 4.5.1\. 让Promise等待指定时间 首先我们来看一下如何在Promise中实现超时。 所谓超时就是要在经过一定时间后进行某些操作,使用 `setTimeout` 的话很好理解。 首先我们来串讲一个单纯的在Promise中调用 `setTimeout` 的函数。 delayPromise.js ~~~ function delayPromise(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } ~~~ `delayPromise(ms)` 返回一个在经过了参数指定的毫秒数后进行onFulfilled操作的promise对象,这和直接使用 `setTimeout` 函数比较起来只是编码上略有不同,如下所示。 ~~~ setTimeout(function () { alert("已经过了100ms!"); }, 100); // == 几乎同样的操作 delayPromise(100).then(function () { alert("已经过了100ms!"); }); ~~~ 在这里 **promise对象** 这个概念非常重要,请切记。 ## 4.5.2\. Promise.race中的超时 让我们回顾一下静态方法 `Promise.race` ,它的作用是在任何一个promise对象进入到确定(解决)状态后就继续进行后续处理,如下面的例子所示。 ~~~ var winnerPromise = new Promise(function (resolve) { setTimeout(function () { console.log('this is winner'); resolve('this is winner'); }, 4); }); var loserPromise = new Promise(function (resolve) { setTimeout(function () { console.log('this is loser'); resolve('this is loser'); }, 1000); }); // 第一个promise变为resolve后程序停止 Promise.race([winnerPromise, loserPromise]).then(function (value) { console.log(value); // => 'this is winner' }); ~~~ 我们可以将刚才的 [delayPromise](http://liubin.github.io/promises-book/#delayPromise.js) 和其它promise对象一起放到 `Promise.race` 中来是实现简单的超时机制。 simple-timeout-promise.js ~~~ function delayPromise(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } function timeoutPromise(promise, ms) { var timeout = delayPromise(ms).then(function () { throw new Error('Operation timed out after ' + ms + ' ms'); }); return Promise.race([promise, timeout]); } ~~~ 函数 `timeoutPromise(比较对象promise, ms)` 接收两个参数,第一个是需要使用超时机制的promise对象,第二个参数是超时时间,它返回一个由 `Promise.race` 创建的相互竞争的promise对象。 之后我们就可以使用 `timeoutPromise` 编写下面这样的具有超时机制的代码了。 ~~~ function delayPromise(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } function timeoutPromise(promise, ms) { var timeout = delayPromise(ms).then(function () { throw new Error('Operation timed out after ' + ms + ' ms'); }); return Promise.race([promise, timeout]); } // 运行示例 var taskPromise = new Promise(function(resolve){ // 随便一些什么处理 var delay = Math.random() * 2000; setTimeout(function(){ resolve(delay + "ms"); }, delay); }); timeoutPromise(taskPromise, 1000).then(function(value){ console.log("taskPromise在规定时间内结束 : " + value); }).catch(function(error){ console.log("发生超时", error); }); ~~~ 虽然在发生超时的时候抛出了异常,但是这样的话我们就不能区分这个异常到底是_普通的错误_还是_超时错误_了。 为了能区分这个 `Error` 对象的类型,我们再来定义一个`Error` 对象的子类 `TimeoutError`。 ## 4.5.3\. 定制Error对象 `Error` 对象是ECMAScript的内建(build in)对象。 但是由于stack trace等原因我们不能完美的创建一个继承自 `Error` 的类,不过在这里我们的目的只是为了和Error有所区别,我们将创建一个 `TimeoutError` 类来实现我们的目的。 > 在ECMAScript6中可以使用 `class` 语法来定义类之间的继承关系。 > ~~~ > class MyError extends Error{ > // 继承了Error类的对象 > } > ~~~ 为了让我们的 `TimeoutError` 能支持类似 `error instanceof TimeoutError` 的使用方法,我们还需要进行如下工作。 TimeoutError.js ~~~ function copyOwnFrom(target, source) { Object.getOwnPropertyNames(source).forEach(function (propName) { Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName)); }); return target; } function TimeoutError() { var superInstance = Error.apply(null, arguments); copyOwnFrom(this, superInstance); } TimeoutError.prototype = Object.create(Error.prototype); TimeoutError.prototype.constructor = TimeoutError; ~~~ 我们定义了 `TimeoutError` 类和构造函数,这个类继承了Error的prototype。 它的使用方法和普通的 `Error` 对象一样,使用 `throw` 语句即可,如下所示。 ~~~ var promise = new Promise(function(){ throw TimeoutError("timeout"); }); promise.catch(function(error){ console.log(error instanceof TimeoutError);// true }); ~~~ 有了这个 `TimeoutError` 对象,我们就能很容易区分捕获的到底是因为超时而导致的错误,还是其他原因导致的Error对象了。 > 本章里介绍的继承JavaScript内建对象的方法可以参考 [Chapter 28. Subclassing Built-ins](http://speakingjs.com/es5/ch28.html) ,那里有详细的说明。此外 [Error - JavaScript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) 也针对Error对象进行了详细说明。 ## 4.5.4\. 通过超时取消XHR操作 到这里,我想各位读者都已经对如何使用Promise来取消一个XHR请求都有一些思路了吧。 取消XHR操作本身的话并不难,只需要调用 `XMLHttpRequest` 对象的 `abort()` 方法就可以了。 为了能在外部调用 `abort()` 方法,我们先对之前本节出现的 [`getURL`](http://liubin.github.io/promises-book/#xhr-promise.js) 进行简单的扩展,`cancelableXHR` 方法除了返回一个包装了XHR的promise对象之外,还返回了一个用于取消该XHR请求的`abort`方法。 delay-race-cancel.js ~~~ function cancelableXHR(URL) { var req = new XMLHttpRequest(); var promise = new Promise(function (resolve, reject) { req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.onabort = function () { reject(new Error('abort this request')); }; req.send(); }); var abort = function () { // 如果request还没有结束的话就执行abort // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest if (req.readyState !== XMLHttpRequest.UNSENT) { req.abort(); } }; return { promise: promise, abort: abort }; } ~~~ 在这些问题都明了之后,剩下只需要进行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 ~~~ function copyOwnFrom(target, source) { Object.getOwnPropertyNames(source).forEach(function (propName) { Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName)); }); return target; } function TimeoutError() { var superInstance = Error.apply(null, arguments); copyOwnFrom(this, superInstance); } TimeoutError.prototype = Object.create(Error.prototype); TimeoutError.prototype.constructor = TimeoutError; function delayPromise(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } function timeoutPromise(promise, ms) { var timeout = delayPromise(ms).then(function () { return Promise.reject(new TimeoutError('Operation timed out after ' + ms + ' ms')); }); return Promise.race([promise, timeout]); } function cancelableXHR(URL) { var req = new XMLHttpRequest(); var promise = new Promise(function (resolve, reject) { req.open('GET', URL, true); req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.onabort = function () { reject(new Error('abort this request')); }; req.send(); }); var abort = function () { // 如果request还没有结束的话就执行abort // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest if (req.readyState !== XMLHttpRequest.UNSENT) { req.abort(); } }; return { promise: promise, abort: abort }; } var object = cancelableXHR('http://httpbin.org/get'); // main timeoutPromise(object.promise, 1000).then(function (contents) { console.log('Contents', contents); }).catch(function (error) { if (error instanceof TimeoutError) { object.abort(); return console.log(error); } console.log('XHR Error :', error); }); ~~~ 上面的代码就通过在一定的时间内变为解决状态的promise对象实现了超时处理。 > 通常进行开发的情况下,由于这些逻辑会频繁使用,因此将这些代码分割保存在不同的文件应该是一个不错的选择。 ## 4.5.5\. promise和操作方法 在前面的 [`cancelableXHR`](http://liubin.github.io/promises-book/#delay-race-cancel.js) 中,promise对象及其操作方法都是在一个对象中返回的,看起来稍微有些不太好理解。 从代码组织的角度来说一个函数只返回一个值(promise对象)是一个非常好的习惯,但是由于在外面不能访问 `cancelableXHR` 方法中创建的 `req` 变量,所以我们需要编写一个专门的函数(上面的例子中的`abort`)来对这些内部对象进行处理。 当然也可以考虑到对返回的promise对象进行扩展,使其支持`abort`方法,但是由于promise对象是对值进行抽象化的对象,如果不加限制的增加操作用的方法的话,会使整体变得非常复杂。 大家都知道一个函数做太多的工作都不认为是一个好的习惯,因此我们不会让一个函数完成所有功能,也许像下面这样对函数进行分割是一个不错的选择。 * 返回包含XHR的promise对象 * 接收promise对象作为参数并取消该对象中的XHR请求 将这些处理整理为一个模块的话,以后扩展起来也方便,一个函数所做的工作也会比较精炼,代码也会更容易阅读和维护。 我们有很多方法来创建一个模块(AMD,CommonJS,ES6 module etc..),在这里,我们将会把前面的 `cancelableXHR` 整理为一个Node.js的模块使用。 cancelableXHR.js ~~~ "use strict"; var requestMap = {}; function createXHRPromise(URL) { var req = new XMLHttpRequest(); var promise = new Promise(function (resolve, reject) { req.open('GET', URL, true); req.onreadystatechange = function () { if (req.readyState === XMLHttpRequest.DONE) { delete requestMap[URL]; } }; req.onload = function () { if (req.status === 200) { resolve(req.responseText); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error(req.statusText)); }; req.onabort = function () { reject(new Error('abort this req')); }; req.send(); }); requestMap[URL] = { promise: promise, request: req }; return promise; } function abortPromise(promise) { if (typeof promise === "undefined") { return; } var request; Object.keys(requestMap).some(function (URL) { if (requestMap[URL].promise === promise) { request = requestMap[URL].request; return true; } }); if (request != null && request.readyState !== XMLHttpRequest.UNSENT) { request.abort(); } } module.exports.createXHRPromise = createXHRPromise; module.exports.abortPromise = abortPromise; ~~~ 使用方法也非常简单,我们通过 `createXHRPromise` 方法得到XHR的promise对象,当想对这个XHR进行`abort`操作的时候,将这个promise对象传递给 `abortPromise(promise)` 方法就可以了。 ~~~ var cancelableXHR = require("./cancelableXHR"); var xhrPromise = cancelableXHR.createXHRPromise('http://httpbin.org/get');//创建包装了XHR的promise对象 xhrPromise.catch(function (error) { // 调用 abort 抛出的错误 }); cancelableXHR.abortPromise(xhrPromise);//取消在1中创建的promise对象的请求操作 ~~~ ## 4.5.6\. 总结 在这里我们学到了如下内容。 * 经过一定时间后变为解决状态的delayPromise * 基于delayPromise和Promise.race的超时实现方式 * 取消XHR promise请求 * 通过模块化实现promise对象和操作的分离 Promise能非常灵活的进行处理流程的控制,为了充分发挥它的能力,我们需要注意不要将一个函数写的过于庞大冗长,而是应该将其分割成更小更简单的处理,并对之前JavaScript中提到的机制进行更深入的了解。
';