title: promise總結
總結在前
前言
下文類似 Promise#then焦除、Promise#resolve 都是Promise的實例對象红伦,
什么是Promise
Promise是抽象異步處理對象以及對其進行各種操作的組件
Promise簡介
目前大致有下面三種類型:
-
1英古、Constructor
var promise = new Promise(function(resolve, reject) {
// 異步處理
// 處理結束后、調(diào)用resolve 或 reject
}); -
2昙读、Instance Method
promise.then(resolved, rejected)
.catch(rejected)
.finally() -
3召调、Static Method
Promise.all()
Promise.resolve()
Promise.reject()
Promise.race()
對比
Promise.resolve()
等價于
new Promise(function(resolve, null){})
Promise的狀態(tài)
用new Promise 實例化的promise對象有以下三個狀態(tài)。
ES6 Promises 規(guī)范中定義的術語 | Promises/A+ 中描述狀態(tài)的術語 | 狀態(tài)說明 |
---|---|---|
has-resolution | Fulfilled | resolve(成功)時蛮浑。此時會調(diào)用 onFulfilled |
has-rejection | Rejected | reject(失敗)時唠叛。此時會調(diào)用 onRejected |
unresolved | Pending | 既不是resolve也不是reject的狀態(tài)。也就是promise對象剛被創(chuàng)建后的初始化狀態(tài)等 |
觸發(fā)Promise之后只會是 resolved 或 rejected 兩種沮稚,而且狀態(tài)不可逆
編寫Promise
創(chuàng)建Promise對象
1.new Promise(fn) 返回一個promise對象
-
2.在fn 中 指定 異步等處理
處理結果正常的話艺沼,調(diào)用resolve(處理結果值)
處理結果錯誤的話,調(diào)用reject(Error對象) -
3.函數(shù)處理 Promise對像
then(); --- 為了避免上述中同時使用同步壮虫、異步調(diào)用可能引起的混亂問題
catch(); --- catch在ie8下存在兼容性問題
finally();
實戰(zhàn)
function getURL (URL) {
return new Promise(function(resolve, reject){
const req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(req.statusText)
}
};
req.onerror = function () {
reject(new Error(req.statusText));
}
req.send();
})
}
實戰(zhàn) Promise中各種方法
Promise.resolve()
返回值是一個promise對象
靜態(tài)方法 Promise.resolve() 可以認為是實例 new Promise(function(resolve, undefined){})方法 的快捷方式(語法糖)
- 讓promise對象立即進入resolved狀態(tài)
將 thenable 對象轉換為promise對象
- 返回值是 thenable
實例:
Promise.resolve(value).then(function(value){
console.log(value);
})
Promise.reject()
返回值是一個promise對象
靜態(tài)方法 Promise.resolve() 可以認為是實例 promise.then(undefined, onRejected)方法 的快捷方式(語法糖)
- 讓promise對象立即進入rejected狀態(tài)
- 使用catch()處理異常
例如:
Promise.reject(new Error("BOOM!")).catch(function(error){
console.error(error);
});
專欄: Promise只能進行異步操作澳厢?
Promise在規(guī)范上規(guī)定 Promise只能使用異步調(diào)用方式思考一個問題:文檔的加載順序都是從上到下加載的,
那么代碼的的位置起何等作用囚似?
先看看Promise的執(zhí)行
var promise = new Promise(function (resolve){
console.log("inner promise"); // 1
resolve(42);
});
promise.then(function(value){
console.log(value); // 3
});
console.log("outer promise"); // 2
執(zhí)行順序: 1 > 2 > 3
同步調(diào)用和異步調(diào)用同時存在導致的混亂
1.正常代碼同時存在 同步 和 異步 的情況:
function onReady (fn) {
const readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function(){
console.log('loaded!')
});
console.log('==starting==');
分析:
如果在調(diào)用onReady之前DOM已經(jīng)載入的話
對回調(diào)函數(shù)進行同步調(diào)用
如果在調(diào)用onReady之前DOM還沒有載入的話
通過注冊 DOMContentLoaded 事件監(jiān)聽器來對回調(diào)函數(shù)進行異步調(diào)用
因此
如果這段代碼在源文件中出現(xiàn)的位置不同剩拢,在控制臺上打印的log消息順序也會不同。
2.使用 setTimeout() 控制代碼 統(tǒng)一異步輸出的情況:
function onReady (fn) {
const readyState = document.readyState;
if (readyState === 'interactive' || readyState === 'complete') {
setTimeout(fn, 0);
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
onReady(function(){
console.log('loaded!')
});
console.log('==starting==');
關于同步 | 異步這個問題饶唤,在 Effective JavaScript 的 第67項 不要對異步回調(diào)函數(shù)進行同步調(diào)用 中也有詳細介紹
- 絕對不能對異步回調(diào)函數(shù)(即使在數(shù)據(jù)已經(jīng)就緒)進行同步調(diào)用
- 如果對異步回調(diào)函數(shù)進行同步調(diào)用的話徐伐,處理順序可能會與預期不符,可能帶來意料之外的后果
- 對異步回調(diào)函數(shù)進行同步調(diào)用募狂,還可能導致棧溢出或異常處理錯亂等問題
- 如果想在將來某時刻調(diào)用異步回調(diào)函數(shù)的話办素,可以使用setTimeout等異步API
Effective JavaScript
---David Herman
前面我們看到的 promise.then 也屬于此類,為了避免上述中同時使用同步祸穷、異步調(diào)用可能引起的混亂問題性穿,Promise在規(guī)范上規(guī)定 Promise只能使用異步調(diào)用方式
3.使用Promise重寫onReady()
function onReadyPromise () {
return new Promise(function(resolve, reject){
const readyState = document.readtState;
if (readyState === 'interactive' || readyState === 'complete') {
resolve();
} else {
window.addEventListener('DOMContentLoaded', resolve);
}
});
}
onReadyPromise().then(function(){
console.log('loaded!);
});
console.log('==starting==');
由于Promise保證了每次調(diào)用都是以異步方式進行的,所以我們在實際編碼中不需要調(diào)用 setTimeout 來自己實現(xiàn)異步調(diào)用雷滚。
Promise#then
Promise方法鏈(Promise Chain)需曾。Promise可以將任意方法寫在一起作為一個方法鏈,所以適合編寫異步處理較多的應用.(越短越好)
如:
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
}
function onRejected(error) {
console.log("Catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
無異常依次輸出:Task A > Task B > Final Task
有異常依次輸出: error > Final Task
Promise Chain 中的傳參原理
Promise Chain 之間的傳參,只需要在每個事務回調(diào)函數(shù)中使用 return 來返回當前值
如:
function doubleUp(value) {
return value * 2;
}
function increment(value) {
value + 1;
}
function output(value) {
console.log(value);// => (1 + 1) * 2
}
var promise = Promise.resolve(1);
promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function(error){
// promise chain中出現(xiàn)異常的時候會被調(diào)用
console.error(error);
});
特點:
- 每個方法中 return 的值不僅只局限于字符串或者數(shù)值類型祈远,也可以是對象或者promise對象等復雜類型呆万。
- return的值會由 Promise.resolve(return的返回值); 進行相應的包裝處理,因此不管回調(diào)函數(shù)中會返回一個什么樣的值车份,最終 then 的結果都是 返回 一個 新創(chuàng)建的promise對象谋减。
- 也就是說, Promise#then 不僅僅是注冊一個回調(diào)函數(shù)那么簡單扫沼,它還會將回調(diào)函數(shù)的返回值進行變換出爹,創(chuàng)建并返回一個promise對象庄吼。
Promise#catch
IE8兼容性
這段代碼在ie8一下出現(xiàn) identifier not found 的語法錯誤:
var promise = Promise.reject(new Error("message"));
promise.catch(function (error) {
console.error(error);
});
這是怎么回事呢? 實際上這和 catch 是ECMAScript的 保留字 (Reserved Word)有關以政。
在ECMAScript 3中保留字是不能作為對象的屬性名使用的霸褒。 而IE8及以下版本都是基于ECMAScript 3實現(xiàn)的,因此不能將 catch 作為屬性來使用盈蛮,也就不能編寫類似 promise.catch() 的代碼,因此就出現(xiàn)了 identifier not found 這種語法錯誤了技矮。
而現(xiàn)在的瀏覽器都是基于ECMAScript 5的抖誉,而在ECMAScript 5中保留字都屬于 IdentifierName ,也可以作為屬性名使用了衰倦。
在ECMAScript5中保留字也不能作為 Identifier 即變量名或方法名使用袒炉。 如果我們定義了一個名為 for 的變量的話,那么就不能和循環(huán)語句的 for 區(qū)分了樊零。 而作為屬性名的話我磁,我們還是很容易區(qū)分 object.for 和 for 的,仔細想想我們就應該能接受將保留字作為屬性名來使用了驻襟。
分析
點標記法(dot notation) 要求對象的屬性必須是有效的標識符(在ECMAScript 3中則不能使用保留字)夺艰,
但是使用 中括號標記法(bracket notation)的話,則可以將非合法標識符作為對象的屬性名使用沉衣。
也就是說郁副,上面的代碼如果像下面這樣重寫的話,就能在IE8及以下版本的瀏覽器中運行了(當然還需要polyfill)豌习。
解決ie8一下兼容性問題
1.使用中括號標記法(bracket notation):
解決Promise#catch標識符沖突問題
var promise = Promise.reject(new Error("message"));
promise["catch"](function (error) {
console.error(error);
});
2.使用Promise#then 替代Promise#catch
var promise = Promise.reject(new Error("message"));
promise.then(undefined, function (error) {
console.error(error);
});
科普:一些庫有關promise.catch的解決方案
由于 catch 標識符可能會導致問題出現(xiàn)存谎,因此一些類庫(Library)也采用了 caught 作為函數(shù)名,而函數(shù)要完成的工作是一樣的肥隆。
而且很多壓縮工具自帶了將 promise.catch 轉換為 promise["catch"] 的功能既荚, 所以可能不經(jīng)意之間也能幫我們解決這個問題。
如果各位讀者需要支持IE8及以下版本的瀏覽器的話栋艳,那么一定要將這個 catch 問題牢記在心中恰聘。
專欄: 每次調(diào)用then 都會 返回 一個新創(chuàng)建的promise對象
測試每次調(diào)用then的返回值
var aPromise = new Promise(function (resolve) {
resolve(100);
});
var thenPromise = aPromise.then(function (value) {
console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true
因此每次都會返回不同Promise對象
所以要留心沒使用Promise時,若要擴展方法時需要返回新的Promise對象進行處理
實戰(zhàn)
// 1: 對同一個promise對象同時調(diào)用 `then` 方法
var aPromise = new Promise(function (resolve) {
resolve(100);
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
return value * 2;
});
aPromise.then(function (value) {
console.log("1: " + value); // => 100
})
// vs
// 2: 對 `then` 進行 promise chain 方式進行調(diào)用
var bPromise = new Promise(function (resolve) {
resolve(100);
});
bPromise.then(function (value) {
return value * 2;
}).then(function (value) {
return value * 2;
}).then(function (value) {
console.log("2: " + value); // => 100 * 2 * 2
});
對比可看出
- 不寫成鏈式情況
promise.then每次接受的參數(shù)都為第一次new時所傳入的數(shù)嘱巾,
因此憨琳,多次promise.then這成立一個異步事務流
- 寫成鏈式情況
寫成鏈式之后,由于每次promise.then都會返回一個新的promise對象旬昭,
則每個then可以通過鏈式調(diào)用來return當前then返回的數(shù)據(jù)或對象
因此篙螟,鏈式調(diào)用是比較推薦的寫法,能避免很多不必要的錯誤
一個有關then的很有代表性的反模式的例子
錯誤做法xxx
function badAsyncCall() {
const promise = Promise.resolve(value);
promise.then(function(value){
return value;
});
return promise;
}
正確做法???
function goodAsyncCall() {
const promise = Promise.resolve(value);
return promise.then(function(value){
return value;
});
}
這種函數(shù)的行為貫穿在Promise整體之中问拘, 包括我們后面要進行說明的 Promise.all 和 Promise.race 遍略,他們都會接收一個promise對象為參數(shù)惧所,并返回一個和接收參數(shù)不同的、新的promise對象绪杏。
Promise和數(shù)組
因為技術不夠下愈,暫時總結不出來,先貼代碼
需要事先說明的是 Promise.all 比較適合這種應用場景的需求蕾久,因此我們故意采用了大量 .then 的晦澀的寫法势似。
使用了.then 的話,也并不是說能和回調(diào)風格完全一致僧著,大概重寫后代碼如下所示履因。
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
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.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
function recordValue(results, value) {
results.push(value);
return results;
}
// [] 用來保存初始化的值
var pushValue = recordValue.bind(null, []);
return request.comment().then(pushValue).then(request.people).then(pushValue);
}
// 運行的例子
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.error(error);
});
Promise.all
接收一個 promise對象的數(shù)組 作為參數(shù),當這個數(shù)組里的所有promise對象全部變?yōu)閞esolve或reject狀態(tài)的時候盹愚,它才會去調(diào)用 .then 方法栅迄。
實戰(zhàn)
function getURL (URL) {
return new Promise(function(resolve, reject){
const req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function(){
if (req.status === 200) {
resolve(req.statusText);
} else {
reject(req.statusText);
}
};
req.onerror = function() {
reject(req.statusText);
}
});
}
const request = {
comment: function getComment (){
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople () {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
}
};
function main () {
return Promise.all([request.comment(), request.people()]);
}
main().then(function(value){
console.log(value);
}).catch(function(value){
console.log(value);
});
Promise.race
使用方法和Promise.all一樣,接收一個promise對象數(shù)組為參數(shù)皆怕。
Promise.all 在接收到的所有的對象promise都變?yōu)?FulFilled 或者 Rejected 狀態(tài)之后才會繼續(xù)進行后面的處理毅舆,
與之相對的是 Promise.race 只要有一個promise對象進入 FulFilled 或者 Rejected 狀態(tài)的話,就會繼續(xù)進行后面的處理愈腾。
實例:
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變?yōu)閞esolve后程序停止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
console.log(value); // => 'this is winner'
});
陸續(xù)輸出: this is winner -> this is loser
也就是說憋活, Promise.race 在第一個promise對象變?yōu)镕ulfilled之后,并不會取消其他promise對象的執(zhí)行顶滩。
在 ES6 Promises 規(guī)范中余掖,也沒有取消(中斷)promise對象執(zhí)行的概念,我們必須要確保promise最終進入resolve or reject狀態(tài)之一礁鲁。也就是說Promise并不適用于 狀態(tài) 可能會固定不變的處理盐欺。也有一些類庫提供了對promise進行取消的操作。
then or catch?
.then 和 .catch 都會創(chuàng)建并返回一個 新的 promise對象仅醇。 Promise實際上每次在方法鏈中增加一次處理的時候所操作的都不是完全相同的promise對象冗美。
案例:
function throwError(value) {
// 拋出異常
throw new Error(value);
}
// <1> onRejected不會被調(diào)用
function badMain(onRejected) {
return Promise.resolve(42).then(throwError, onRejected);
}
// <2> 有異常發(fā)生時onRejected會被調(diào)用
function goodMain(onRejected) {
return Promise.resolve(42).then(throwError).catch(onRejected);
}
// 運行示例
badMain(function(){
console.log("BAD");
});
goodMain(function(){
console.log("GOOD");
});
輸出結果:“GOOD”,
而且badMain沒有運行
分析
在上述例子中析二,Promise.resolve(42)接受了value后返回 一個新的Promise對象A粉洼,接著then通過鏈式接受A對象并分別指定回調(diào)函數(shù)里面的參數(shù)resolve和reject;
對于badMain函數(shù)叶摄,then方法回調(diào)函數(shù)參數(shù)里面的的resolve傳入一個threwError函數(shù)E并拋出異常属韧,在回調(diào)參數(shù)里面的的resolve傳入onRejected函數(shù)。因為異常函數(shù)傳入then參數(shù)的resolve蛤吓,reosolve拋出的異常需要在下一個then的參數(shù)reject 或 在then后面的catch來接收異常宵喂,所以badMain函數(shù)沒有被調(diào)用;
對于goodMain函數(shù)会傲,then方法回調(diào)函數(shù)參數(shù)的resolve拋出異常锅棕,并有then后面的catch函數(shù)接受異常函數(shù)E拙泽,所以goodMain被調(diào)用并正常打印成功。
總結
這里我們又學習到了如下一些內(nèi)容裸燎。
1.使用promise.then(onFulfilled, onRejected) 的話
- 在 onFulfilled 中發(fā)生異常的話顾瞻,在 onRejected 中是捕獲不到這個異常的。
2.在 promise.then(onFulfilled).catch(onRejected) 的情況下
- then 中產(chǎn)生的異常能在 .catch 中捕獲
3..then 和 .catch 在本質(zhì)上是沒有區(qū)別的
- 需要分場合使用德绿。
我們需要注意如果代碼類似 badMain 那樣的話荷荤,就可能出現(xiàn)程序不會按預期運行的情況,從而不能正確的進行錯誤處理脆炎。
Promise測試
總結編寫Promise 的測試代碼
1.使用Mocha測試框架
現(xiàn)在使用 Mocha來對Promise 進行基本的測試
使用Mocha的主要基于下面3點理由:
- 它是非常著名的測試框架
- 支持基于Node.js 和瀏覽器的測試
- 支持"Promise測試"
Macha簡介
Mocha可以自由選擇BDD梅猿、TDD、exports中的任意風格秒裕,測試中用到的Assert 方法也同樣可以跟任何其他類庫組合使用。 也就是說钞啸,Mocha本身只提供執(zhí)行測試時的框架几蜻,而其他部分則由使用者自己選擇
- Mocha官網(wǎng)
- Mocha是Node.js下的測試框架工具
Macha-回調(diào)函數(shù)風格的測試
//basic-test.js
var assert = require('power-assert');
describe('Basic Test', function () {
context('When Callback(high-order function)', function () {
it('should use `done` for test', function (done) {
setTimeout(function () {
assert(true);
done();
}, 0);
});
});
context('When promise object', function () {
it('should use `done` for test?', function (done) {
var promise = Promise.resolve(1);
// このテストコードはある欠陥があります
promise.then(function (value) {
assert(value === 1);
done();
});
});
});
});
Promise進階(Advanced)
Promise的實現(xiàn)類庫(Library)
ES6 Promise 里關于promise對象的規(guī)定包括在使用 catch 方法慎颗,或使用 Promise.all 進行處理的時候不能出現(xiàn)錯誤矿筝。
Promises/A+ 是 ES6 Promises 的前身漂辐,Promise的 then 也是來自于此的基于社區(qū)的規(guī)范歧蕉。
如果說一個類庫兼容 Promises/A+ 的話庸追,那么就是說它除了具有標準的 then 方法之外摆霉,很多情況下也說明此類庫還支持 Promise.all 和 catch 等功能涧黄。
但是 Promises/A+ 實際上只是定義了關于 Promise#then 的規(guī)范波丰,所以有些類庫可能實現(xiàn)了其它諸如 all 或 catch 等功能蹬敲,但是可能名字卻不一樣暇昂。
如果我們說一個類庫具有 then 兼容性的話,實際上指的是 Thenable 伴嗡,它通過使用 Promise.resolve 基于ES6 Promise的規(guī)定急波,進行promise對象的變換。
Polyfill和擴展類庫
在這些Promise的實現(xiàn)類庫中瘪校,我們這里主要對兩種類型的類庫進行介紹澄暮。
一種是被稱為 Polyfill (這是一款英國產(chǎn)品,就是裝修刮墻用的膩子阱扬,其意義可想而知?—?譯者注)的類庫泣懊,另一種是即具有 Promises/A+兼容性 ,又增加了自己獨特功能的類庫麻惶。
Promise的實現(xiàn)類庫數(shù)量非常之多馍刮,這里我們只是介紹了其中有限的幾個。
1.Polyfill
只需要在瀏覽器中加載Polyfill類庫用踩,就能使用IE10等或者還沒有提供對Promise支持的瀏覽器中使用Promise里規(guī)定的方法渠退。
也就是說如果加載了Polyfill類庫忙迁,就能在還不支持Promise的環(huán)境中,運行本文中的各種示例代碼碎乃。
-
jakearchibald/es6-promise
一個兼容 ES6 Promises 的Polyfill類庫姊扔。 它基于 RSVP.js 這個兼容 Promises/A+ 的類庫, 它只是 RSVP.js 的一個子集梅誓,只實現(xiàn)了Promises 規(guī)定的 API恰梢。 -
yahoo/ypromise
這是一個獨立版本的 YUI 的 Promise Polyfill,具有和 ES6 Promises 的兼容性梗掰。 本書的示例代碼也都是基于這個 ypromise 的 Polyfill 來在線運行的嵌言。 -
getify/native-promise-only
以作為ES6 Promises的polyfill為目的的類庫 它嚴格按照ES6 Promises的規(guī)范設計,沒有添加在規(guī)范中沒有定義的功能及穗。 如果運行環(huán)境有原生的Promise支持的話摧茴,則優(yōu)先使用原生的Promise支持。
2.Promise擴展類庫
Promise擴展類庫除了實現(xiàn)了Promise中定義的規(guī)范之外埂陆,還增加了自己獨自定義的功能苛白。
Promise擴展類庫數(shù)量非常的多,我們只介紹其中兩個比較有名的類庫焚虱。
Q 和 Bluebird 這兩個類庫除了都能在瀏覽器里運行之外购裙,充實的API reference也是其特征。
kriskowal/q
類庫 Q 實現(xiàn)了 Promises 和 Deferreds 等規(guī)范鹃栽。 它自2009年開始開發(fā)躏率,還提供了面向Node.js的文件IO API Q-IO 等, 是一個在很多場景下都能用得到的類庫民鼓。API Reference · kriskowal/q Wiki
Q等文檔里詳細介紹了Q的Deferred和jQuery里的Deferred有哪些異同薇芝,以及要怎么進行遷移 Coming from jQuery 等都進行了詳細的說明。petkaantonov/bluebird
這個類庫除了兼容 Promise 規(guī)范之外摹察,還擴展了取消promise對象的運行恩掷,取得promise的運行進度,以及錯誤處理的擴展檢測等非常豐富的功能供嚎,此外它在實現(xiàn)上還在性能問題下了很大的功夫黄娘。bluebird/API.md at master · petkaantonov/bluebird
Bluebird的文檔除了提供了使用Promise豐富的實現(xiàn)方式之外,還涉及到了在出現(xiàn)錯誤時的對應方法以及 Promise中的反模式 等內(nèi)容克滴。
這兩個類庫的文檔寫得都很友好逼争,即使我們不使用這兩個類庫,閱讀一下它們的文檔也具有一定的參考價值劝赔。
Promise.resolve和Thenable
Promise.resolve 的最大特征之一就是可以將thenable的對象轉換為promise對象∈慕梗現(xiàn)在總結一下利用將thenable對象轉換為promise對象這個功能都能具體做些什么事情。下面是案例:
1.將Web Notifications轉換為thenable對象
使用方法:
new Notification("Hi!");
Notification API 的使用步驟:
用戶在這個是否允許Notification的對話框選擇后的結果,會通過 Notification.permission 傳給我們的程序杂伟,它的值可能是允許("granted")或拒絕("denied")這二者之一移层。
-
顯示是否允許通知的對話框,并異步處理用戶選擇結果赫粥。resolve(成功)時 == 用戶允許("granted")
如果用戶允許的話观话,則通過 new Notification 顯示通知消息。這又分兩種情況
用戶之前已經(jīng)允許過
-
當場彈出是否允許桌面通知對話框越平。reject(失敗)時 == 用戶拒絕("denied")
- 當用戶不允許的時候频蛔,不執(zhí)行任何操作
實戰(zhàn):
1.以回調(diào)函數(shù)編寫為例
notification-callback.js
funtion notifyMessage(msg, opt, callback) {
if(Notification && Notification.permission === 'granted'){
const notification = new Notification(msg, opt);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
const notification = new Notification(msg, opt);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
// 運行實例
// 第二個參數(shù)是傳給 `Notification` 的option對象
notifyMessage("Hi!", {}, function (error, notification) {
if(error){
return console.error(error);
}
console.log(notification);// 通知對象
});
2.對比使用回調(diào)函數(shù),下面以promise重寫
notification-as-promise.js
function notifyMessage(msg, opt, callback) {
if (Notification && Notification.permission === 'granted') {
const notification = new Notification(msg, opt);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function(status){
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
const notification = new Notification(msg, opt);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
function notifyMessagePromise(msg opt) {
return new Promise(function(resolve, reject){
notifyMessage(msg, opt, function(error, notification){
if (error) {
reject(error);
} else {
resolve(notification);
}
});
});
}
notifyMessagePromise("Hi!").then(function(notification){
console.log(notification);
}).catch(function(error){
console.log(error);
});
3.Web Notifications As Thenable秦叛。thenable就是一個具有 .then方法的一個對象
notification-thenable.js
function notifyMessage(message, options, callback) {
if (Notification && Notification.permission === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else if (Notification.requestPermission) {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
if (status === 'granted') {
var notification = new Notification(message, options);
callback(null, notification);
} else {
callback(new Error('user denied'));
}
});
} else {
callback(new Error('doesn\'t support Notification API'));
}
}
// 返回 `thenable`
function notifyMessageAsThenable(msg, opt) {
return {
'then': function(resolve, reject){
notifyMessage(msg, opt, function(error, notification) {
if (error) {
reject(error);
} else {
resolve(notification);
}
});
}
};
}
// 運行實例
Promise.resolve(notifyMessageAsThenable('Hi!', opt)).then(function(notification){
console.log(notification);
}).catch(function(error){
consoe.log(error);
});
notification-thenable.js
里增加了一個 notifyMessageAsThenable
方法晦溪。這個方法返回的對象具備一個then方法
。
then
方法的參數(shù)和 new Promise(function (resolve, reject){})
一樣挣跋,在確定時執(zhí)行 resolve
方法三圆,拒絕時調(diào)用 reject
方法。
then
方法和 notification-as-promise.js
中的 notifyMessageAsPromise
方法完成了同樣的工作避咆。
我們可以看出嫌术, Promise.resolve(thenable)
通過使用了 thenable
這個promise對象,就能利用Promise功能了牌借。
Promise.resolve(notifyMessageAsThenable("message")).then(function (notification) {
console.log(notification);// 通知對象
}).catch(function(error){
console.error(error);
});
使用了Thenable的notification-thenable.js 和依賴于Promise的 notification-as-promise.js ,實際上都是非常相似的使用方法割按。
notification-thenable.js 和 notification-as-promise.js比起來膨报,有以下的不同點:
- 類庫側沒有提供 Promise 的實現(xiàn)
- 用戶通過 Promise.resolve(thenable) 來自己實現(xiàn)了 Promise
- 作為Promise使用的時候,需要和 Promise.resolve(thenable) 一起配合使用
通過使用Thenable對象适荣,我們可以實現(xiàn)類似已有的回調(diào)式風格和Promise風格中間的一種實現(xiàn)風格现柠。
4.總結
在本小節(jié)我們主要學習了什么是Thenable,以及如何通過Promise.resolve(thenable)
使用Thenable弛矛,將其作為promise對象來使用够吩。
Callback?—?Thenable?—?Promise
Thenable風格表現(xiàn)為位于回調(diào)和Promise風格中間的一種狀態(tài),作為類庫的公開API有點不太成熟丈氓,所以并不常見周循。
Thenable本身并不依賴于Promise
功能,但是Promise之外也沒有使用Thenable的方式万俗,所以可以認為Thenable間接依賴于Promise湾笛。
另外,用戶需要對 Promise.resolve(thenable)
有所理解才能使用好Thenable闰歪,因此作為類庫的公開API有一部分會比較難嚎研。和公開API相比,更多情況下是在內(nèi)部使用Thenable库倘。
在編寫異步處理的類庫的時候临扮,推薦采用先編寫回調(diào)風格的函數(shù)论矾,然后再轉換為公開API這種方式。
貌似Node.js的Core module就采用了這種方式杆勇,除了類庫提供的基本回調(diào)風格的函數(shù)之外贪壳,用戶也可以通過Promise或者Generator等自己擅長的方式進行實現(xiàn)。
最初就是以能被Promise使用為目的的類庫靶橱,或者其本身依賴于Promise等情況下寥袭,我想將返回promise對象的函數(shù)作為公開API應該也沒什么問題。
什么時候該使用Thenable关霸?
那么传黄,又是在什么情況下應該使用Thenable呢?
恐怕最可能被使用的是在 Promise類庫 之間進行相互轉換了队寇。
比如膘掰,類庫Q的Promise實例為Q promise對象,提供了 ES6 Promises 的promise對象不具備的方法佳遣。Q promise對象提供了 promise.finally(callback)
和 promise.nodeify(callback)
等方法识埋。
如果你想將ES6 Promises的promise對象轉換為Q promise的對象,輪到Thenable大顯身手的時候就到了零渐。
使用thenable將promise對象轉換為Q promise對象
var Q = require("Q");
// 這是一個ES6的promise對象
var promise = new Promise(function(resolve){
resolve(1);
});
// 變換為Q promise對象
Q(promise).then(function(value){
console.log(value);
}).finally(function(){
console.log("finally");
});
因為是Q promise對象所以可以使用 finally
方法
上面代碼中最開始被創(chuàng)建的promise對象具備then
方法窒舟,因此是一個Thenable對象。我們可以通過Q(thenable)
方法诵盼,將這個Thenable對象轉換為Q promise對象惠豺。
可以說它的機制和 Promise.resolve(thenable)
一樣,當然反過來也一樣风宁。
像這樣洁墙,Promise類庫雖然都有自己類型的promise對象,但是它們之間可以通過Thenable這個共通概念戒财,在類庫之間(當然也包括native Promise)進行promise對象的相互轉換热监。
我們看到,就像上面那樣饮寞,Thenable多在類庫內(nèi)部實現(xiàn)中使用孝扛,所以從外部來說不會經(jīng)常看到Thenable的使用骂际。但是我們必須牢記Thenable是Promise中一個非常重要的概念疗琉。
使用reject而不是throw
Promise的構造函數(shù),以及被 then 調(diào)用執(zhí)行的函數(shù)基本上都可以認為是在 try...catch 代碼塊中執(zhí)行的歉铝,所以在這些代碼中即使使用 throw 盈简,程序本身也不會因為異常而終止。
那么使用reject還是throw?先看看代碼對比
// throw實例
const promise = new Promise(function(resolve, reject){
throw new Error('error');
});
promise.catch(function(error){
console.log(error);
});
如果在Promise中使用 throw 語句的話柠贤,會被 try...catch 住香浩,最終promise對象也變?yōu)镽ejected狀態(tài)
// reject實例
const promise = new Promise(function(resolve, reject){
reject(new Error("message"));
});
promise.catch(function(error){
console.error(error);// => "message"
})
對比上面案例,相對于throw臼勉,使用reject更加直觀邻吭、合理
使用reject有什么優(yōu)點?
1.首先是因為我們很難區(qū)分 throw 是我們主動拋出來的宴霸,還是因為真正的其它 異常 導致的囱晴。
- 比如在使用Chrome瀏覽器的時候,Chrome的開發(fā)者工具提供了在程序發(fā)生異常的時候自動在調(diào)試器中break的功能瓢谢。
當我們開啟這個功能的時候畸写,在執(zhí)行到下面代碼中的 throw 時就會觸發(fā)調(diào)試器的break行為。
var promise = new Promise(function(resolve, reject){
throw new Error("message");
});
本來這是和調(diào)試沒有關系的地方氓扛,也因為在Promise中的 throw 語句被break了枯芬,這也嚴重的影響了瀏覽器提供的此功能的正常使用。
2.在then中進行reject
關于使用Promise進行超時處理的具體實現(xiàn)方法可以參考 使用Promise.race和delay取消XHR請求 中的詳細說明采郎。
現(xiàn)在用then來處理超時
const promise = Promise.resolve()l
promise.then(function(){
const retPromise = new Promise(function(resolve, reject){
setTimeout(function(){
// 經(jīng)過一段時間后還沒處理完的話就進行reject
});
// 比較耗時的處理 - 1
somethingHardWork();
});
return retPromise;
}).then(onFulfilled, onRejected);
原理分析:因為then
在注冊回調(diào)函數(shù)的時候可以通過return
一個值或對象來傳給后面的then
或catch
中的回調(diào)函數(shù)處理千所,因此可以使用then中return
的特性來解決。提倡使用Promise.race和delay取消XHR請求
3.總結
- 使用 reject 會比使用 throw 安全
- 在 then 中使用reject的方法
Deferred和Promise
這里的Deferred
指具有js延遲的一類庫(Library)
1.Deferred和Promise的關系
- Deferred 擁有 Promise
- Deferred 具備對 Promise的狀態(tài)進行操作的特權方法(圖中的"特権メソッド")
我想各位看到此圖應該就很容易理解了蒜埋,Deferred和Promise并不是處于競爭的關系淫痰,而是Deferred內(nèi)涵了Promise。
這是jQuery.Deferred結構的簡化版整份。當然也有的Deferred實現(xiàn)并沒有內(nèi)涵Promise黑界。
Deferred最初是在Python的 Twisted 框架中被提出來的概念。 在JavaScript領域可以認為它是由 MochiKit.Async 皂林、 dojo/Deferred 等Library引入的。
2.實例Deferred top on Promise
基于Promise實現(xiàn)的deferred
// deferred.js
function Deferred(){
this.promise = new Promise(function(resolve, reject){
this._resolve = resolve;
this._reject = reject;
}).bind(this);
}
Deferred.prototype.resolve = function(value){
this._resolve.call(this.promise, value);
}
Deferred.prototype.reject = function(reason){
this._reject.call.call(this.promise, reason);
}
使用Promise實現(xiàn)的 getURL 用Deferred改寫一下
// xhr-deferred.js
function Deferred(){
this.promise = new Promise(function(resolve, reject){
this._resolve = resolve;
this._reject = reject;
}).bind(this);
}
Deferred.prototype.resolve = function(value){
this._resolve.call(this.promise, value);
}
Deferred.prototype.reject = function(reason){
this._reject.call.call(this.promise, reason);
}
function getURL(URL) {
const deferred = new Deferred();
const req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function(){
if (req.status === 200) {
deferred.resolve(req.reponseText);
} else {
deferred.reject(req.reponseText);
}
};
req.onerror = function() {
deferred.reject(new Error(req.statusText));
};
req.send();
return deferred.promise;
}
// 運行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(console.error.bind(console));
所謂的能對Promise狀態(tài)進行操作的
特權方法
蚯撩,指的就是能對promise對象的狀態(tài)進行resolve础倍、reject等調(diào)用的方法,而通常的Promise的話只能在通過構造函數(shù)傳遞的方法之內(nèi)對promise對象的狀態(tài)進行操作胎挎。
我們來看看Deferred和Promise相比在實現(xiàn)上有什么異同沟启。
xhr-promise.js
function getURL(URL) {
return new Promise(function(resolve, reject){
const req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function(){
if (req.status === 200) {
resolve(req.reponseText);
} else {
reject(req.reponseText);
}
}
req.onerror = function() {
reject(new Error(req.statusText));
};
req.send();
});
}
// 運行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(console.error.bind(console));
對比上述兩個版本的 getURL
,我們發(fā)現(xiàn)它們有如下不同犹菇。
- Deferred 的話不需要將代碼用Promise括起來
- 由于沒有被嵌套在函數(shù)中德迹,可以減少一層縮進
- 反過來沒有Promise里的錯誤處理邏輯
在以下方面,它們則完成了同樣的工作揭芍。
- 整體處理流程
- 調(diào)用 resolve胳搞、reject 的時機
- 函數(shù)都返回了promise對象
由于Deferred包含了Promise,所以大體的流程還是差不多的,不過Deferred有用對Promise進行操作的特權方法肌毅,以及高度自由的對流程控制進行自由定制筷转。
比如在Promise
一般都會在構造函數(shù)中編寫主要處理邏輯,對 resolve悬而、reject 方法的調(diào)用時機也基本是很確定的呜舒。
new Promise(function (resolve, reject){
// 在這里進行promise對象的狀態(tài)確定
});
而使用Deferred
的話,并不需要將處理邏輯寫成一大塊代碼笨奠,只需要先創(chuàng)建deferred對象袭蝗,可以在任何時機對 resolve、reject 方法進行調(diào)用般婆。
var deferred = new Deferred();
// 可以在隨意的時機對 `resolve`到腥、`reject` 方法進行調(diào)用
上面我們只是簡單的實現(xiàn)了一個 Deferred ,我想你已經(jīng)看到了它和 Promise 之間的差異了吧腺兴。
如果說
Promise
是用來對值
進行抽象
的話左电,Deferred
則是對
處理還沒有結束的狀態(tài)
或操作進行抽象化的對象
,我們也可以從這一層的區(qū)別來理解一下這兩者之間的差異页响。
換句話說篓足,Promise代表了一個對象,這個對象的狀態(tài)現(xiàn)在還不確定闰蚕,但是未來一個時間點它的狀態(tài)要么變?yōu)檎V担‵ulFilled)栈拖,要么變?yōu)楫惓V担≧ejected);而Deferred對象表示了一個處理還沒有結束的這種事實没陡,在它的處理結束的時候涩哟,可以通過Promise來取得處理結果。
使用Promise.race和delay取消XHR請求
當然XHR有一個 timeout 屬性盼玄,使用該屬性也可以簡單實現(xiàn)超時功能贴彼,但是為了能支持多個XHR同時超時或者其他功能,我們采用了容易理解的異步方式在XHR中通過超時來實現(xiàn)取消正在進行中的操作埃儿。
如何使用Promise.race來實現(xiàn)超時機制器仗?
1.讓Promise等待指定時間
首先我們來串講一個單純的在Promise中調(diào)用 setTimeout 的函數(shù)。
delayPromise.js
function delayPromise(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
delayPromise(ms) 返回一個在經(jīng)過了參數(shù)指定的毫秒數(shù)后進行onFulfilled操作的promise對象童番,這和直接使用 setTimeout 函數(shù)比較起來只是編碼上略有不同精钮,如下所示。
setTimeout(function () {
alert("已經(jīng)過了100ms剃斧!");
}, 100);
// == 幾乎同樣的操作
delayPromise(100).then(function () {
alert("已經(jīng)過了100ms轨香!");
});
在這里 promise對象 這個概念非常重要,請切記幼东。
2.Promise.race中的超時
它的作用是在任何一個promise對象進入到確定(解決)狀態(tài)后就繼續(xù)進行后續(xù)處理揖庄,如下面的例子所示
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變?yōu)閞esolve后程序停止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
console.log(value); // => 'this is winner'
});
我們可以將剛才的 delayPromise 和其它promise對象一起放到 Promise.race
中來是實現(xiàn)簡單的超時機制。
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]);
}
函數(shù) timeoutPromise(比較對象promise, ms)
接收兩個參數(shù)凝垛,第一個是需要使用超時機制的promise對象,第二個參數(shù)是超時時間炸渡,它返回一個由 Promise.race
創(chuàng)建的相互競爭的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在規(guī)定時間內(nèi)結束 : " + value);
}).catch(function(error){
console.log("發(fā)生超時", error);
});
雖然在發(fā)生超時的時候拋出了異常蚌堵,但是這樣的話我們就不能區(qū)分這個異常到底是普通的錯誤還是超時錯誤了。
為了能區(qū)分這個 Error
對象的類型沛婴,我們再來定義一個Error
對象的子類 TimeoutError
吼畏。
3.定制Error對象
Error
對象是ECMAScript的內(nèi)建(build in)對象。
但是由于stack trace等原因我們不能完美的創(chuàng)建一個繼承自 Error
的類嘁灯,不過在這里我們的目的只是為了和Error有所區(qū)別泻蚊,我們將創(chuàng)建一個 TimeoutError
類來實現(xiàn)我們的目的。
在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
對象羹奉,我們就能很容易區(qū)分捕獲的到底是因為超時而導致的錯誤秒旋,還是其他原因導致的Error對象了
4.通過超時取消XHR操作
到這里,我想各位讀者都已經(jīng)對如何使用Promise來取消一個XHR請求都有一些思路了吧诀拭。
取消XHR操作本身的話并不難迁筛,只需要調(diào)用 XMLHttpRequest
對象的 abort()
方法就可以了。
為了能在外部調(diào)用 abort()
方法耕挨,我們先對之前本節(jié)出現(xiàn)的 getURL
進行簡單的擴展细卧,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還沒有結束的話就執(zhí)行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在超時前返回結果的話
a.和正常的promise一樣插勤,通過
then
返回請求結果 -
發(fā)生超時的時候
a.拋出
throw TimeoutError
異常并被catch
b.catch的錯誤對象如果是
TimeoutError
類型的話,則調(diào)用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() {
const superInstance = Error.apply(null, arguments);
copyOwnFrom(this, superInstance);
}
TimeoutError.prototype = Object.create(Error,prototype);
TimeoutError.prototype.constructor = TimeourError;
function delayPromise(ms){
return new Promise(function(resolve){
setTimeout(function(resolve, ms));
});
}
function timeoutPromise(promise, ms){
let timeout = delayPromise(ms).then(function(){
return Promise.reject(new TimeoutError('Operation timed out after' + ms + 'ms'));
});
return Promise.race([promise, timeout]);
}
function cancelableXHR(URL) {
const req = new XMLHttpRequest();
const promise = new Promise(function(resolve, reject){
req.open('GET', URL, true);
req.onload = function(){
if (req.status === 200) {
resolve(req.statusText);
} else {
reject(req.statusText);
}
};
req.onerror = function() {
reject(new Error(req.statusText));
};
req.abort = function() {
reject(new Error('abort this request'));
};
});
const abort = function() {
if (req.readyState !== XMLHttpRequest.UNSENT) {
req.abort();
}
};
return {
promise: promise,
abort: abort
};
}
var object = cancelableXHR('http://httpbin.org/get');
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);
});
上面的代碼就通過在一定的時間內(nèi)變?yōu)榻鉀Q狀態(tài)的promise對象實現(xiàn)了超時處理。
通常進行開發(fā)的情況下析恋,由于這些邏輯會頻繁使用良哲,因此將這些代碼分割保存在不同的文件應該是一個不錯的選擇。
- promise和操作方法
在前面的 cancelableXHR
中助隧,promise對象及其操作方法都是在一個對象中返回的筑凫,看起來稍微有些不太好理解滑沧。
從代碼組織的角度來說一個函數(shù)只返回一個值(promise對象)是一個非常好的習慣,但是由于在外面不能訪問 cancelableXHR 方法中創(chuàng)建的 req 變量巍实,所以我們需要編寫一個專門的函數(shù)(上面的例子中的abort)來對這些內(nèi)部對象進行處理滓技。
當然也可以考慮到對返回的promise對象進行擴展,使其支持abort方法棚潦,但是由于promise對象是對值進行抽象化的對象令漂,如果不加限制的增加操作用的方法的話,會使整體變得非常復雜丸边。
大家都知道一個函數(shù)做太多的工作都不認為是一個好的習慣叠必,因此我們不會讓一個函數(shù)完成所有功能,也許像下面這樣對函數(shù)進行分割是一個不錯的選擇妹窖。
返回包含XHR的promise對象
接收promise對象作為參數(shù)并取消該對象中的XHR請求
將這些處理整理為一個模塊的話纬朝,以后擴展起來也方便,一個函數(shù)所做的工作也會比較精煉骄呼,代碼也會更容易閱讀和維護共苛。
我們有很多方法來創(chuàng)建一個模塊(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');
xhrPromise.catch(function (error) {
// 調(diào)用 abort 拋出的錯誤
});
cancelableXHR.abortPromise(xhrPromise);
- 總結
在這里我們學到了如下內(nèi)容耻蛇。
經(jīng)過一定時間后變?yōu)榻鉀Q狀態(tài)的delayPromise
基于delayPromise和Promise.race的超時實現(xiàn)方式
取消XHR promise請求
通過模塊化實現(xiàn)promise對象和操作的分離
Promise能非常靈活的進行處理流程的控制踪蹬,為了充分發(fā)揮它的能力,我們需要注意不要將一個函數(shù)寫的過于龐大冗長臣咖,而是應該將其分割成更小更簡單的處理跃捣,并對之前JavaScript中提到的機制進行更深入的了解。
什么是 Promise.prototype.done 夺蛇?
then和done的異常區(qū)別
- then交給catch捕獲處理
- done直接拋出去
如果你使用過其他的Promise實現(xiàn)類庫的話疚漆,可能見過用done
代替then
的例子。
這些類庫都提供了 Promise.prototype.done
方法刁赦,使用起來也和 then
一樣娶聘,但是這個方法并不會返回promise對象。
雖然 ES6 Promises和Promises/A+等在設計上并沒有對Promise.prototype.done 做出任何規(guī)定甚脉,但是很多實現(xiàn)類庫都提供了該方法的實現(xiàn)丸升。
在本小節(jié)中,我們將會學習什么是 Promise.prototype.done
牺氨,以及為什么很多類庫都提供了對該方法的支持狡耻。
- 使用done的代碼示例
promise-done-example.js
if (typeof Promise.prototype.done === 'undefined') {
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected).catch(function (error) {
setTimeout(function () {
throw error;
}, 0);
});
};
}
var promise = Promise.resolve();
promise.done(function () {
JSON.parse('this is not json'); // => SyntaxError: JSON.parse
});
// => 請打開瀏覽器的開發(fā)者工具中的控制臺窗口看一下
在前面我們已經(jīng)說過墩剖,promise設計規(guī)格并沒有對 Promise.prototype.done做出任何規(guī)定,因此在使用的時候夷狰,你可以使用已有類庫提供的實現(xiàn)岭皂,也可以自己去實現(xiàn)。
我們會在后面講述如何去自己實現(xiàn)沼头,首先我們這里先對使用 then 和使用 done這兩種方式進行一下比較爷绘。
使用then的場景
var promise = Promise.resolve();
promise.then(function () {
JSON.parse("this is not json");
}).catch(function (error) {
console.error(error);// => "SyntaxError: JSON.parse"
});
從上面我們可以看出,兩者之間有以下不同點瘫证。
-
done 并不返回promise對象
- 也就是說揉阎,在done之后不能使用 catch 等方法組成方法鏈
-
done 中發(fā)生的異常會被直接拋給外面
- 也就是說,不會進行Promise的錯誤處理(Error Handling)
由于done 不會返回promise對象背捌,所以我們不難理解它只能出現(xiàn)在一個方法鏈的最后毙籽。
此外,我們已經(jīng)介紹過了Promise具有強大的錯誤處理機制毡庆,而done則會在函數(shù)中跳過錯誤處理坑赡,直接拋出異常。
為什么很多類庫都提供了這個和Promise功能相矛盾的函數(shù)呢么抗?看一下下面Promise處理失敗的例子毅否,也許我們多少就能理解其中原因了吧。
- 消失的錯誤
Promise雖然具備了強大的錯誤處理機制蝇刀,但是(調(diào)試工具不能順利運行的時候)這個功能會導致人為錯誤(human error)更加復雜螟加,這也是它的一個缺點。
也許你還記得吞琐,我們在 then or catch? 中也看到了類似的內(nèi)容捆探。
像下面那樣,我們看一個能返回promise對象的函數(shù)站粟。
json-promise.js
function JSONPromise(value) {
return new Promise(function (resolve) {
resolve(JSON.parse(value));
});
}
這個函數(shù)將接收到的參數(shù)傳遞給 JSON.parse
黍图,并返回一個基于JSON.parse的promise對象。
我們可以像下面那樣使用這個Promise函數(shù)奴烙,由于 JSON.parse 會解析失敗并拋出一個異常助被,該異常會被 catch 捕獲。
function JSONPromise(value) {
return new Promise(function (resolve) {
resolve(JSON.parse(value));
});
}
// 運行示例
var string = "非合法json編碼字符串";
JSONPromise(string).then(function (object) {
console.log(object);
}).catch(function(error){
// => JSON.parse拋出異常時
console.error(error);
});
如果這個解析失敗的異常被正常捕獲的話則沒什么問題切诀,但是如果編碼時忘記了處理該異常揩环,一旦出現(xiàn)異常,那么查找異常發(fā)生的源頭將會變得非常棘手幅虑,這就是使用promise需要注意的一面丰滑。
忘記了使用catch進行異常處理的的例子
var string = "非合法json編碼字符串";
JSONPromise(string).then(function (object) {
console.log(object);
});
雖然拋出了異常,但是沒有對該異常進行處理
如果是JSON.parse 這樣比較好找的例子還算好說翘单,如果是拼寫錯誤的話吨枉,那么發(fā)生了Syntax Error錯誤的話將會非常麻煩。
typo錯誤
var string = "{}";
JSONPromise(string).then(function (object) {
conosle.log(object);
});
存在conosle這個拼寫錯誤
這這個例子里哄芜,我們錯把 console 拼成了 conosle 貌亭,因此會發(fā)生如下錯誤。
ReferenceError: conosle is not defined
但是认臊,由于Promise的try-catch機制圃庭,這個問題可能會被內(nèi)部消化掉。 如果在調(diào)用的時候每次都無遺漏的進行 catch 處理的話當然最好了失晴,但是如果在實現(xiàn)的過程中出現(xiàn)了這個例子中的錯誤的話剧腻,那么進行錯誤排除的工作也會變得困難。
這種錯誤被內(nèi)部消化的問題也被稱為 unhandled rejection 涂屁,從字面上看就是在Rejected時沒有找到相應處理的意思书在。
這種unhandled rejection錯誤到底有多難檢查,也依賴于Promise的實現(xiàn)拆又。 比如 ypromise 在檢測到 unhandled rejection 錯誤的時候儒旬,會在控制臺上提示相應的信息。
Promise rejected but no error handlers were registered to it
另外帖族, Bluebird 在比較明顯的人為錯誤栈源,即ReferenceError等錯誤的時候,會直接顯示到控制臺上竖般。
"Possibly unhandled ReferenceError. conosle is not defined
原生(Native)的 Promise實現(xiàn)為了應對同樣問題甚垦,提供了GC-based unhandled rejection tracking功能。
該功能是在promise對象被垃圾回收器回收的時候涣雕,如果是unhandled rejection的話艰亮,則進行錯誤顯示的一種機制。
Firefox 或 Chrome 的原生Promise都進行了部分實現(xiàn)胞谭。
4.6.3. done的實現(xiàn)
作為方法論垃杖,在Promise中 done 是怎么解決上面提到的錯誤被忽略呢? 其實它的方法很簡單直接丈屹,那就是必須要進行錯誤處理调俘。
由于可以在 Promise上實現(xiàn) done 方法,因此我們看看如何對 Promise.prototype.done 這個Promise的prototype進行擴展旺垒。
promise-prototype-done.js
"use strict";
if (typeof Promise.prototype.done === "undefined") {
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected).catch(function (error) {
setTimeout(function () {
throw error;
}, 0);
});
};
}
那么它是如何將異常拋到Promise的外面的呢彩库?其實這里我們利用的是在setTimeout中使用throw方法,直接將異常拋給了外部先蒋。
setTimeout的回調(diào)函數(shù)中拋出異常
try{
setTimeout(function callback() {
throw new Error("error");
}, 0);
}catch(error){
console.error(error);
}
這個例外不會被捕獲
關于為什么異步的callback中拋出的異常不會被捕獲的原因骇钦,可以參考下面內(nèi)容。
JavaScript和異步錯誤處理 - Yahoo! JAPAN Tech Blog(日語博客)
仔細看一下 Promise.prototype.done的代碼竞漾,我們會發(fā)現(xiàn)這個函數(shù)什么也沒 return 眯搭。 也就是說窥翩, done按照「Promise chain在這里將會中斷,如果出現(xiàn)了異常鳞仙,直接拋到promise外面即可」的原則進行了處理寇蚊。
如果實現(xiàn)和運行環(huán)境實現(xiàn)的比較完美的話,就可以進行 unhandled rejection 檢測棍好,done也不一定是必須的了仗岸。 另外像本小節(jié)中的 Promise.prototype.done一樣,done也可以在既有的Promise之上進行實現(xiàn)借笙,也可以說它沒有進入到 ES6 Promises的設計規(guī)范之中扒怖。
- 總結
在本小節(jié)中,我們學習了 Q 业稼、 Bluebird 和 prfun 等Promise類庫提供的 done 的基礎和實現(xiàn)細節(jié)盗痒,以及done方法和 then 方法有什么區(qū)別等內(nèi)容。
我們也學到了 done 有以下兩個特點盼忌。
done 中出現(xiàn)的錯誤會被作為異常拋出
終結 Promise chain
和 then or catch? 中說到的一樣积糯,由Promise內(nèi)部消化掉的錯誤,隨著調(diào)試工具或者類庫的改進谦纱,大多數(shù)情況下也許已經(jīng)不是特別大的問題了看成。
此外,由于 done 不會有返回值跨嘉,因此不能在它之后進行方法鏈的創(chuàng)建川慌,為了實現(xiàn)Promise方法風格上的統(tǒng)一,我們也可以使用done方法祠乃。
ES6 Promises 本身提供的功能并不是特別多梦重。 因此,我想很多時候可能需要我們自己進行擴展或者使用第三方類庫亮瓷。
我們好不容易將異步處理統(tǒng)一采用Promise進行統(tǒng)一處理琴拧,但是如果做過頭了,也會將系統(tǒng)變得特別復雜嘱支,因此蚓胸,保持風格的統(tǒng)一是Promise作為抽象對象非常重要的部分。
Promise和方法鏈(method chain)
在Promise中你可以將 then 和 catch 等方法連在一起寫除师。這非常像DOM或者jQuery中的方法鏈沛膳。
一般的方法鏈都通過返回 this 將多個方法串聯(lián)起來。
另一方面汛聚,由于Promise 每次都會返回一個新的promise對象 锹安,所以從表面上看和一般的方法鏈幾乎一模一樣。
在本小節(jié)里,我們會在不改變已有采用方法鏈編寫的代碼的外部接口的前提下叹哭,學習如何在內(nèi)部使用Promise進行重寫忍宋。
- fs中的方法鏈
我們下面將會以 Node.js中的fs 為例進行說明。
此外风罩,這里的例子我們更重視代碼的易理解性讶踪,因此從實際上來說這個例子可能并不算太實用。
fs-method-chain.js
"use strict";
var fs = require("fs");
function File() {
this.lastValue = null;
}
// Static method for File.prototype.read
File.read = function FileRead(filePath) {
var file = new File();
return file.read(filePath);
};
File.prototype.read = function (filePath) {
this.lastValue = fs.readFileSync(filePath, "utf-8");
return this;
};
File.prototype.transform = function (fn) {
this.lastValue = fn.call(this, this.lastValue);
return this;
};
File.prototype.write = function (filePath) {
this.lastValue = fs.writeFileSync(filePath, this.lastValue);
return this;
};
module.exports = File;
這個模塊可以將類似下面的 read → transform → write 這一系列處理泊交,通過組成一個方法鏈來實現(xiàn)。
var File = require("./fs-method-chain");
var inputFilePath = "input.txt",
outputFilePath = "output.txt";
File.read(inputFilePath)
.transform(function (content) {
return ">>" + content;
})
.write(outputFilePath);
transform 接收一個方法作為參數(shù)柱查,該方法對其輸入?yún)?shù)進行處理廓俭。在這個例子里,我們對通過read讀取的數(shù)據(jù)在前面加上了 >> 字符串唉工。
- 基于Promise的fs方法鏈
下面我們就在不改變剛才的方法鏈對外接口的前提下研乒,采用Promise對內(nèi)部實現(xiàn)進行重寫。
fs-promise-chain.js
"use strict";
var fs = require("fs");
function File() {
this.promise = Promise.resolve();
}
// Static method for File.prototype.read
File.read = function (filePath) {
var file = new File();
return file.read(filePath);
};
File.prototype.then = function (onFulfilled, onRejected) {
this.promise = this.promise.then(onFulfilled, onRejected);
return this;
};
File.prototype["catch"] = function (onRejected) {
this.promise = this.promise.catch(onRejected);
return this;
};
File.prototype.read = function (filePath) {
return this.then(function () {
return fs.readFileSync(filePath, "utf-8");
});
};
File.prototype.transform = function (fn) {
return this.then(fn);
};
File.prototype.write = function (filePath) {
return this.then(function (data) {
return fs.writeFileSync(filePath, data)
});
};
module.exports = File;
新增加的then 和catch都可以看做是指向內(nèi)部保存的promise對象的別名淋硝,而其它部分從對外接口的角度來說都沒有改變雹熬,使用方法也和原來一樣。
因此谣膳,在使用這個模塊的時候我們只需要修改 require 的模塊名即可竿报。
var File = require("./fs-promise-chain");
var inputFilePath = "input.txt",
outputFilePath = "output.txt";
File.read(inputFilePath)
.transform(function (content) {
return ">>" + content;
})
.write(outputFilePath);
File.prototype.then 方法會調(diào)用 this.promise.then 方法,并將返回的promise對象賦值給了 this.promise 變量這個內(nèi)部promise對象继谚。
這究竟有什么奧妙么烈菌?通過以下的偽代碼,我們可以更容易理解這背后發(fā)生的事情花履。
var File = require("./fs-promise-chain");
File.read(inputFilePath)
.transform(function (content) {
return ">>" + content;
})
.write(outputFilePath);
// => 處理流程類似以下的偽代碼
promise.then(function read(){
return fs.readFileSync(filePath, "utf-8");
}).then(function transform(content) {
return ">>" + content;
}).then(function write(){
return fs.writeFileSync(filePath, data);
});
看到 promise = promise.then(...) 這種寫法芽世,會讓人以為promise的值會被覆蓋,也許你會想是不是promise的chain被截斷了诡壁。
你可以想象為類似 promise = addPromiseChain(promise, fn); 這樣的感覺济瓢,我們?yōu)閜romise對象增加了新的處理,并返回了這個對象妹卿,因此即使自己不實現(xiàn)順序處理的話也不會帶來什么問題旺矾。
- 兩者的區(qū)別
同步和異步
要說fs-method-chain.js和Promise版兩者之間的差別,最大的不同那就要算是同步和異步了纽帖。
如果在類似 fs-method-chain.js 的方法鏈中加入隊列等處理的話宠漩,就可以實現(xiàn)幾乎和異步方法鏈同樣的功能,但是實現(xiàn)將會變得非常復雜懊直,所以我們選擇了簡單的同步方法鏈扒吁。
Promise版的話如同在 專欄: Promise只能進行異步處理?里介紹過的一樣,只會進行異步操作雕崩,因此使用了promise的方法鏈也是異步的魁索。
錯誤處理
雖然fs-method-chain.js里面并不包含錯誤處理的邏輯, 但是由于是同步操作盼铁,因此可以將整段代碼用 try-catch 包起來粗蔚。
在 Promise版 提供了指向內(nèi)部promise對象的then 和 catch 別名,所以我們可以像其它promise對象一樣使用catch來進行錯誤處理饶火。
fs-promise-chain中的錯誤處理
var File = require("./fs-promise-chain");
File.read(inputFilePath)
.transform(function (content) {
return ">>" + content;
})
.write(outputFilePath)
.catch(function(error){
console.error(error);
});
如果你想在fs-method-chain.js中自己實現(xiàn)異步處理的話鹏控,錯誤處理可能會成為比較大的問題;可以說在進行異步處理的時候肤寝,還是使用Promise實現(xiàn)起來比較簡單当辐。
- Promise之外的異步處理
如果你很熟悉Node.js的話,那么看到方法鏈的話鲤看,你是不是會想起來 Stream 呢炉奴。
如果使用 Stream 的話昆烁,就可以免去了保存 this.lastValue 的麻煩,還能改善處理大文件時候的性能。 另外司倚,使用Stream的話可能會比使用Promise在處理速度上會快些枫疆。
使用Stream進行read→transform→write
readableStream.pipe(transformStream).pipe(writableStream);
因此馍惹,在異步處理的時候并不是說Promise永遠都是最好的選擇燎猛,要根據(jù)自己的目的和實際情況選擇合適的實現(xiàn)方式。
Node.js的Stream是一種基于Event的技術
關于Node.js中Stream的詳細信息可以參考以下網(wǎng)頁溉瓶。
利用Node.js Stream API對數(shù)據(jù)進行流式處理 - Block Rockin’ Codes
Stream2基礎
關于Node-v0.12新功能
- Promise wrapper
再回到 fs-method-chain.js 和 Promise版陆赋,這兩種方法相比較內(nèi)部實現(xiàn)也非常相近,讓人覺得是不是同步版本的代碼可以直接就當做異步方式來使用呢嚷闭?
由于JavaScript可以向對象動態(tài)添加方法攒岛,所以從理論上來說應該可以從非Promise版自動生成Promise版的代碼。(當然靜態(tài)定義的實現(xiàn)方式容易處理)
盡管 ES6 Promises 并沒有提供此功能胞锰,但是著名的第三方Promise實現(xiàn)類庫 bluebird 等提供了被稱為 Promisification 的功能灾锯。
如果使用類似這樣的類庫,那么就可以動態(tài)給對象增加promise版的方法嗅榕。
var fs = Promise.promisifyAll(require("fs"));
fs.readFileAsync("myfile.js", "utf8").then(function(contents){
console.log(contents);
}).catch(function(e){
console.error(e.stack);
});
Array的Promise wrapper
前面的 Promisification 具體都干了些什么光憑想象恐怕不太容易理解顺饮,我們可以通過給原生的 Array 增加Promise版的方法為例來進行說明。
在JavaScript中原生DOM或String等也提供了很多創(chuàng)建方法鏈的功能凌那。 Array 中就有諸如 map 和 filter 等方法兼雄,這些方法會返回一個數(shù)組類型,可以用這些方法方便的組建方法鏈帽蝶。
array-promise-chain.js
"use strict";
function ArrayAsPromise(array) {
this.array = array;
this.promise = Promise.resolve();
}
ArrayAsPromise.prototype.then = function (onFulfilled, onRejected) {
this.promise = this.promise.then(onFulfilled, onRejected);
return this;
};
ArrayAsPromise.prototype["catch"] = function (onRejected) {
this.promise = this.promise.catch(onRejected);
return this;
};
Object.getOwnPropertyNames(Array.prototype).forEach(function (methodName) {
// Don't overwrite
if (typeof ArrayAsPromise[methodName] !== "undefined") {
return;
}
var arrayMethod = Array.prototype[methodName];
if (typeof arrayMethod !== "function") {
return;
}
ArrayAsPromise.prototype[methodName] = function () {
var that = this;
var args = arguments;
this.promise = this.promise.then(function () {
that.array = Array.prototype[methodName].apply(that.array, args);
return that.array;
});
return this;
};
});
module.exports = ArrayAsPromise;
module.exports.array = function newArrayAsPromise(array) {
return new ArrayAsPromise(array);
};
原生的 Array 和 ArrayAsPromise 在使用時有什么差異呢赦肋?我們可以通過對 上面的代碼 進行測試來了解它們之間的不同點。
array-promise-chain-test.js
"use strict";
var assert = require("power-assert");
var ArrayAsPromise = require("../src/promise-chain/array-promise-chain");
describe("array-promise-chain", function () {
function isEven(value) {
return value % 2 === 0;
}
function double(value) {
return value * 2;
}
beforeEach(function () {
this.array = [1, 2, 3, 4, 5];
});
describe("Native array", function () {
it("can method chain", function () {
var result = this.array.filter(isEven).map(double);
assert.deepEqual(result, [4, 8]);
});
});
describe("ArrayAsPromise", function () {
it("can promise chain", function (done) {
var array = new ArrayAsPromise(this.array);
array.filter(isEven).map(double).then(function (value) {
assert.deepEqual(value, [4, 8]);
}).then(done, done);
});
});
});
我們看到,在 ArrayAsPromise 中也能使用 Array的方法佃乘。而且也和前面的例子類似囱井,原生的Array是同步處理,而 ArrayAsPromise 則是異步處理趣避,這也是它們的不同之處庞呕。
仔細看一下 ArrayAsPromise 的實現(xiàn),也許你已經(jīng)注意到了程帕, Array.prototype 的所有方法都被實現(xiàn)了住练。 但是,Array.prototype 中也存在著類似array.indexOf 等并不會返回數(shù)組類型數(shù)據(jù)的方法愁拭,這些方法如果也要支持方法鏈的話就有些不自然了澎羞。
在這里非常重要的一點是,我們可以通過這種方式敛苇,為具有接收相同類型數(shù)據(jù)接口的API動態(tài)的創(chuàng)建Promise版的API。 如果我們能意識到這種API的規(guī)則性的話顺呕,那么就可能發(fā)現(xiàn)一些新的使用方法枫攀。
前面我們看到的 Promisification 方法,借鑒了了 Node.js的Core模塊中在進行異步處理時將 function(error,result){} 方法的第一個參數(shù)設為 error 這一規(guī)則株茶,自動的創(chuàng)建由Promise包裝好的方法来涨。
- 總結
在本小節(jié)我們主要學習了下面的這些內(nèi)容。
Promise版的方法鏈實現(xiàn)
Promise并不是總是異步編程的最佳選擇
Promisification
統(tǒng)一接口的重用
ES6 Promises只提供了一些Core級別的功能启盛。 因此蹦掐,我們也許需要對現(xiàn)有的方法用Promise方式重新包裝一下。
但是僵闯,類似Event等調(diào)用次數(shù)沒有限制的回調(diào)函數(shù)等在并不適合使用Promise卧抗,Promise也不能說什么時候都是最好的選擇。
至于什么情況下應該使用Promise鳖粟,什么時候不該使用Promise社裆,并不是本書要討論的目的碰纬, 我們需要牢記的是不要什么都用Promise去實現(xiàn)涎拉,我想最好根據(jù)自己的具體目的和情況,來考慮是應該使用Promise還是其它方法朱沃。
使用Promise進行順序(sequence)處理
在第2章 Promise.all 中榄攀,我們已經(jīng)學習了如何讓多個promise對象同時開始執(zhí)行的方法嗜傅。
但是 Promise.all 方法會同時運行多個promise對象,如果想進行在A處理完成之后再開始B的處理檩赢,對于這種順序執(zhí)行的話 Promise.all就無能為力了吕嘀。
此外,在同一章的Promise和數(shù)組 中,我們也介紹了一種效率不是特別高的币他,使用了 重復使用多個then的方法 來實現(xiàn)如何按順序進行處理坞靶。
在本小節(jié)中,我們將對如何在Promise中進行順序處理進行介紹蝴悉。
- 循環(huán)和順序處理
在 重復使用多個then的方法 中的實現(xiàn)方法如下彰阴。
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
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.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
function recordValue(results, value) {
results.push(value);
return results;
}
// [] 用來保存初始化的值
var pushValue = recordValue.bind(null, []);
return request.comment().then(pushValue).then(request.people).then(pushValue);
}
// 運行示例
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.error(error);
});
使用這種寫法的話那么隨著 request 中元素數(shù)量的增加,我們也需要不斷增加對 then 方法的調(diào)用
因此拍冠,如果我們將處理內(nèi)容統(tǒng)一放到數(shù)組里尿这,再配合for循環(huán)進行處理的話,那么處理內(nèi)容的增加將不會再帶來什么問題庆杜。首先我們就使用for循環(huán)來完成和前面同樣的處理射众。
promise-foreach-xhr.js
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
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.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
function recordValue(results, value) {
results.push(value);
return results;
}
// [] 用來保存初始化值
var pushValue = recordValue.bind(null, []);
// 返回promise對象的函數(shù)的數(shù)組
var tasks = [request.comment, request.people];
var promise = Promise.resolve();
// 開始的地方
for (var i = 0; i < tasks.length; i++) {
var task = tasks[i];
promise = promise.then(task).then(pushValue);
}
return promise;
}
// 運行示例
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.error(error);
});
使用for循環(huán)的時候,如同我們在 專欄: 每次調(diào)用then都會返回一個新創(chuàng)建的promise對象 以及 Promise和方法鏈 中學到的那樣晃财,每次調(diào)用 Promise#then 方法都會返回一個新的promise對象叨橱。
因此類似 promise = promise.then(task).then(pushValue); 的代碼就是通過不斷對promise進行處理,不斷的覆蓋 promise 變量的值断盛,以達到對promise對象的累積處理效果罗洗。
但是這種方法需要 promise 這個臨時變量,從代碼質(zhì)量上來說顯得不那么簡潔钢猛。
如果將這種循環(huán)寫法改用 Array.prototype.reduce 的話伙菜,那么代碼就會變得聰明多了。
- Promise chain和reduce
如果將上面的代碼用 Array.prototype.reduce 重寫的話命迈,會像下面一樣贩绕。
promise-reduce-xhr.js
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
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.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
var tasks = [request.comment, request.people];
return tasks.reduce(function (promise, task) {
return promise.then(task).then(pushValue);
}, Promise.resolve());
}
// 運行示例
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.error(error);
});
這段代碼中除了 main 函數(shù)之外的其他處理都和使用for循環(huán)的時候相同。
Array.prototype.reduce 的第二個參數(shù)用來設置盛放計算結果的初始值壶愤。在這個例子中淑倾, Promise.resolve() 會賦值給 promise ,此時的 task 為 request.comment 征椒。
在reduce中第一個參數(shù)中被 return 的值踊淳,則會被賦值為下次循環(huán)時的 promise 。也就是說陕靠,通過返回由 then 創(chuàng)建的新的promise對象迂尝,就實現(xiàn)了和for循環(huán)類似的 Promise chain 了。
下面是關于 Array.prototype.reduce 的詳細說明剪芥。
Array.prototype.reduce() - JavaScript | MDN
azu / Array.prototype.reduce Dance - Glide
使用reduce和for循環(huán)不同的地方是reduce不再需要臨時變量 promise 了垄开,因此也不用編寫 promise = promise.then(task).then(pushValue); 這樣冗長的代碼了,這是非常大的進步税肪。
雖然 Array.prototype.reduce 非常適合用來在Promise中進行順序處理溉躲,但是上面的代碼有可能讓人難以理解它是如何工作的榜田。
因此我們再來編寫一個名為 sequenceTasks 的函數(shù),它接收一個數(shù)組作為參數(shù)锻梳,數(shù)組里面存放的是要進行的處理Task箭券。
從下面的調(diào)用代碼中我們可以非常容易的從其函數(shù)名想到,該函數(shù)的功能是對 tasks 中的處理進行順序執(zhí)行了疑枯。
var tasks = [request.comment, request.people];
sequenceTasks(tasks);
- 定義進行順序處理的函數(shù)
基本上我們只需要基于 使用reduce的方法 重構出一個函數(shù)辩块。
promise-sequence.js
function sequenceTasks(tasks) {
function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
return tasks.reduce(function (promise, task) {
return promise.then(task).then(pushValue);
}, Promise.resolve());
}
需要注意的一點是,和 Promise.all 等不同荆永,這個函數(shù)接收的參數(shù)是一個函數(shù)的數(shù)組废亭。
為什么傳給這個函數(shù)的不是一個promise對象的數(shù)組呢?這是因為promise對象創(chuàng)建的時候具钥,XHR已經(jīng)開始執(zhí)行了豆村,因此再對這些promise對象進行順序處理的話就不能正常工作了。
因此 sequenceTasks 將函數(shù)(該函數(shù)返回一個promise對象)的數(shù)組作為參數(shù)骂删。
最后掌动,使用 sequenceTasks 重寫最開始的例子的話,如下所示宁玫。
promise-sequence-xhr.js
function sequenceTasks(tasks) {
function recordValue(results, value) {
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
return tasks.reduce(function (promise, task) {
return promise.then(task).then(pushValue);
}, Promise.resolve());
}
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
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.send();
});
}
var request = {
comment: function getComment() {
return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
},
people: function getPeople() {
return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
}
};
function main() {
return sequenceTasks([request.comment, request.people]);
}
// 運行示例
main().then(function (value) {
console.log(value);
}).catch(function(error){
console.error(error);
});
怎樣粗恢, main() 中的流程是不是更清晰易懂了。
如上所述撬统,在Promise中,我們可以選擇多種方法來實現(xiàn)處理的按順序執(zhí)行敦迄。
循環(huán)使用then調(diào)用的方法
使用for循環(huán)的方法
使用reduce的方法
分離出順序處理函數(shù)的方法
但是恋追,這些方法都是基于JavaScript中對數(shù)組及進行操作的for循環(huán)或 forEach 等,本質(zhì)上并無大區(qū)別罚屋。 因此從一定程度上來說苦囱,在處理Promise的時候,將大塊的處理分成小函數(shù)來實現(xiàn)是一個非常好的實踐脾猛。
- 總結
在本小節(jié)中撕彤,我們對如何在Promise中進行和 Promise.all 相反,按順序讓promise一個個進行處理的實現(xiàn)方式進行了介紹猛拴。
為了實現(xiàn)順序處理羹铅,我們也對從過程風格的編碼方式到自定義順序處理函數(shù)的方式等實現(xiàn)方式進行了介紹,也再次強調(diào)了在Promise領域我們應遵循將處理按照函數(shù)進行劃分的基本原則愉昆。
在Promise中如果還使用了Promise chain將多個處理連接起來的話职员,那么還可能使源代碼中的一條語句變得很長。
這時候如果我們回想一下這些編程的基本原則進行函數(shù)拆分的話跛溉,代碼整體結構會變得非常清晰焊切。
此外,Promise的構造函數(shù)以及 then 都是高階函數(shù)扮授,如果將處理分割為函數(shù)的話,還能得到對函數(shù)進行靈活組合使用的副作用专肪,意識到這一點對我們也會有一些幫助的刹勃。
高階函數(shù)指的是一個函數(shù)可以接受其參數(shù)為函數(shù)對象的實例