在本小節中,作為在第[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中提到的機制進行更深入的了解。
- 前言
- 第一章 - 什么是Promise
- 1.1. 什么是Promise
- 1.2. Promise簡介
- 1.3. 編寫Promise代碼
- 第二章 - 實戰Promise
- 2.1. Promise.resolve
- 2.2. Promise.reject
- 2.3. 專欄: Promise只能進行異步操作?
- 2.4. Promise#then
- 2.5. Promise#catch
- 2.6. 專欄: 每次調用then都會返回一個新創建的promise對象
- 2.7. Promise和數組
- 2.8. Promise.all
- 2.9. Promise.race
- 2.10. then or catch?
- 第三章 - Promise測試
- 3.1. 基本測試
- 3.2. Mocha對Promise的支持
- 3.3. 編寫可控測試(controllable tests)
- 第四章 - Advanced
- 4.1. Promise的實現類庫(Library)
- 4.2. Promise.resolve和Thenable
- 4.3. 使用reject而不是throw
- 4.4. Deferred和Promise
- 4.5. 使用Promise.race和delay取消XHR請求
- 4.6. 什么是 Promise.prototype.done ?
- 4.7. Promise和方法鏈(method chain)
- 4.8. 使用Promise進行順序(sequence)處理
- 第五章 - Promises API Reference
- 5.1. Promise#then
- 5.2. Promise#catch
- 5.3. Promise.resolve
- 5.4. Promise.reject
- 5.5. Promise.all
- 5.6. Promise.race
- 第六章 - 用語集
- 第七章 - 參考網站
- 第八章 - 關于作者
- 第九章 - 關于譯者