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中提到的机制进行更深入的了解。
';