javascript Promises

原文地址:http://www.html5rocks.com/zh/tutorials/es6/promises/#toc-api

女士們先生們,請準備好迎接 Web 開發歷史上一個重大時刻……

[鼓聲響起]

JavaScript 有了原生的 Promise!

[漫天的煙花綻放,人羣沸騰了]

這時候你大概是這三種人之一:

你的身邊擁擠着歡呼的人羣,但是你卻不在其中,甚至你還不大清楚“Promise”是什麼。你聳聳肩,煙花的碎屑在你的身邊落下。這樣的話,不要擔心,我也是花了多年的時間才明白 Promise 的意義,你可以從這裏開始看起。
你一揮拳!太讚了對麼!你已經用過一些 Promise 的庫,但是所有這些第三方實現在API上都略有差異,JavaScript 官方的 API 會是什麼樣子?看這裏!
你早就知道了,看着那些歡呼雀躍的新人你的嘴角泛起一絲不屑的微笑。你可以安靜享受一會兒優越感,然後直接去看 API 參考吧。
他們都在激動什麼?
JavaScript 是單線程的,這意味着任何兩句代碼都不能同時運行,它們得一個接一個來。在瀏覽器中,JavaScript 和其他任務共享一個線程,不同的瀏覽器略有差異,但大體上這些和 JavaScript 共享線程的任務包括重繪、更新樣式、用戶交互等,所有這些任務操作都會阻塞其他任務。

作爲人類,你是多線程的。你可以用多個手指同時敲鍵盤,也可以一邊開車一遍電話。唯一的全局阻塞函數是打噴嚏,打噴嚏期間所有其他事務都會暫停。很煩人對麼?尤其是當你開着車打着電話的時候。我們都不喜歡這樣打噴嚏的代碼。

你應該會用事件加回調的辦法來處理這類情況:

var img1 = document.querySelector(‘.img-1’);
img1.addEventListener(‘load’, function() {
// 啊哈圖片加載完成
});
img1.addEventListener(‘error’, function() {
// 哎喲出問題了
});
這樣就不打噴嚏了。我們添加幾個監聽函數,請求圖片,然後 JavaScript 就停止運行了,直到觸發某個監聽函數。

上面的例子中唯一的問題是,事件有可能在我們綁定監聽器之前就已經發生,所以我們先要檢查圖片的“complete”屬性:

var img1 = document.querySelector(‘.img-1’);
function loaded() {
// 啊哈圖片加載完成
}
if (img1.complete) {
loaded();
}else {
img1.addEventListener(‘load’, loaded);
}
img1.addEventListener(‘error’, function() {
// 哎喲出問題了
});
這樣還不夠,如果在添加監聽函數之前圖片加載發生錯誤,我們的監聽函數還是白費,不幸的是 DOM 也沒有爲這個需求提供解決辦法。而且,這還只是處理一張圖片的情況,如果有一堆圖片要處理那就更麻煩了。

事件不是萬金油
事件機制最適合處理同一個對象上反覆發生的事情—— keyup、touchstart 等等。你不需要考慮當綁定監聽器之前所發生的事情,當碰到異步請求成功/失敗的時候,你想要的通常是這樣:

img1.callThisIfLoadedOrWhenLoaded(function() {
// 加載完成
}).orIfFailedCallThis(function() {
// 加載失敗
});
// 以及……
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// 全部加載完成
}).orIfSomeFailedCallThis(function() {
// 一個或多個加載失敗
});
這就是 Promise。如果 HTML 圖片元素有一個“ready()”方法的話,我們就可以這樣:

img1.ready().then(function() {
// 加載完成
}, function() {
// 加載失敗
});
// 以及……
Promise.all([img1.ready(), img2.ready()]).then(function() {
// 全部加載完成
}, function() {
// 一個或多個加載失敗
});
基本上 Promise 還是有點像事件回調的,除了:

一個 Promise 只能成功或失敗一次,並且狀態無法改變(不能從成功變爲失敗,反之亦然)
如果一個 Promise 成功或者失敗之後,你爲其添加針對成功/失敗的回調,則相應的回調函數會立即執行
這些特性非常適合處理異步操作的成功/失敗情景,你無需再擔心事件發生的時間點,而只需對其做出響應。

Promise 相關術語
Domenic Denicola 審閱了本文初稿,給我在術語方面打了個“F”,關了禁閉並且責令我打印 States and Fates 一百遍,還寫了一封家長信給我父母。即便如此,我還是對術語有些迷糊,不過基本上應該是這樣:

一個 Promise 的狀態可以是:

肯定(fulfilled)該 Promise 對應的操作成功了
否定(rejected)該 Promise 對應的操作失敗了
等待(pending)還沒有得到肯定或者否定結果,進行中
結束(settled)已經肯定或者否定了
規範裏還使用 thenable 來描述一個對象是否是“類 Promise”(擁有名爲“then”的方法)的。這個術語使我想起來前英格蘭足球經理 Terry Venables 所以我儘量少用它。

JavaScript 有了 Promise!
其實已經有一些第三方庫實現了 Promise 的功能:

Q
when
WinJS
RSVP.js
上面這些庫和 JavaScript 原生 Promise 都遵守一個通用的、標準化的規範:Promises/A+,jQuery 有個類似的方法叫 Deferred,但不兼容 Promises/A+ 規範,於是會有點小問題,使用需謹慎。jQuery 還有一個 Promise 類型,它其實是 Deferred 的縮減版,所以也有同樣問題。

儘管 Promise 的各路實現遵循同一規範,它們的 API 還是各不相同。JavaScript Promise 的 API 比較接近 RSVP.js,如下創建 Promise

var promise = new Promise(function(resolve, reject) {
// 做一些異步操作的事情,然後……

if (/* 一切正常 */) {
resolve(“Stuff worked!”);
}
else {
reject(Error(“It broke”));
}});
Promise 的構造器接受一個函數作爲參數,它會傳遞給這個回調函數兩個變量 resolve 和 reject。在回調函數中做一些異步操作,成功之後調用 resolve,否則調用 reject。

調用 reject 的時候傳遞給它一個 Error 對象只是個慣例並非必須,這和經典 JavaScript 中的 throw 一樣。傳遞 Error 對象的好處是它包含了調用堆棧,在調試的時候會有點用處。

現在來看看如何使用 Promise:

promise.then(function(result) {
console.log(result); // “完美!”
}, function(err) {
console.log(err); // Error: “出問題了”
});
“then”接受兩個參數,成功的時候調用一個,失敗的時候調用另一個,兩個都是可選的,所以你可以只處理成功的情況或者失敗的情況。

JavaScript Promise 最初以“Futures”爲名歸爲 DOM 規範,後來改名爲“Promises”,最終納入 JavaScript 規範。將其加入 JavaScript 而非 DOM 的好處是方便在非瀏覽器環境中使用,如Node.js(他們會不會在覈心API中使用就是另一回事了)。

儘管它被歸位 JavaScript 特性,DOM 也很樂意拿過來用。事實上,所有包含異步成功/失敗方法的新 DOM API 都會使用 Promise 機制,已經實現的包括 Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams 等等。

瀏覽器支持和 Polyfill
目前的瀏覽器已經(部分)實現了 Promise。

Chrome 32、Opera 19 和 Firefox 29 以上的版本已經默認支持 Promise。由於是在 WebKit 內核中所以我們有理由期待下個版本的 Safari 也會支持,並且 IE 也在不斷的開發中。

要在這兩個瀏覽器上達到兼容標準 Promise,或者在其他瀏覽器以及 Node.js 中使用 Promise,可以看看這個 polyfill(gzip 之後 2K)。

與其他庫的兼容性
JavaScript Promise 的 API 會把任何包含有 then 方法的對象當作“類 Promise”(或者用術語來說就是 thenable。嘆氣)的對象,所以如果你使用的庫返回一個 Q Promise,那沒問題,無縫融入新的 JavaScript Promise。

儘管,如前所述,jQuery 的 Deferred 對象有點……沒什麼用,不過幸好還可以轉換成標準 Promise,你最好一拿到對象就馬上加以轉換:

var jsPromise = Promise.resolve(.ajax(/whatever.json));jQuery .ajax 返回一個 Deferred 對象,含有“then”方法,因此Promise.resolve 可以將其轉換爲 JavaScript Promise。不過有時候 Deferred 對象會給它的回調函數傳遞多個參數,例如:

var jqDeferred = $.ajax(‘/whatever.json’);
jqDeferred.then(function(response, statusText, xhrObj) {
// …
}, function(xhrObj, textStatus, err) {
// …
});
除了第一個參數,其他都會被 JavaScript Promise 忽略掉:

jsPromise.then(function(response) {
// …
}, function(xhrObj) {
// …
});
……還好這通常就是你想要的了,至少你能夠用這個辦法實現想要的。另外還要注意,jQuery 也沒有遵循給否定回調函數傳遞 Error 對象的慣例。

複雜的異步代碼變得簡單了
OK,現在我們來寫點實際的代碼。假設我們想要:

顯示一個加載指示圖標
加載一篇小說的 JSON,包含小說名和每一章內容的 URL。
在頁面中填上小說名
加載所有章節正文
在頁面中添加章節正文
停止加載指示
……這個過程中如果發生什麼錯誤了要通知用戶,並且把加載指示停掉,不然它就會不停轉下去,令人眼暈,或者搞壞界面什麼的。

當然了,你不會用 JavaScript 去這麼繁瑣地顯示一篇文章,直接輸出 HTML 要快得多,不過這個流程是非常典型的 API 請求模式:獲取多個數據,當它們全部完成之後再做一些事情。

首先,搞定從網絡加載數據的步驟:

將 Promise 用於 XMLHttpRequest
只要能保持向後兼容,現有 API 都會更新以支持 Promise,XMLHttpRequest 是重點考慮對象之一。不過現在我們先來寫個 GET 請求:

function get(url) {
// 返回一個新的 Promise
return new Promise(function(resolve, reject) {
// 經典 XHR 操作
var req = new XMLHttpRequest();
req.open(‘GET’, url);

req.onload = function() {
  // 當發生 404 等狀況的時候調用此函數
  // 所以先檢查狀態碼
  if (req.status == 200) {
    // 以響應文本爲結果,完成此 Promise
    resolve(req.response);
  }
  else {
    // 否則就以狀態碼爲結果否定掉此 Promise
    // (提供一個有意義的 Error 對象)
    reject(Error(req.statusText));
  }
};

// 網絡異常的處理方法
req.onerror = function() {
  reject(Error("Network Error"));
};

// 發出請求
req.send();

});}
現在可以調用它了:

get(‘story.json’).then(function(response) {
console.log(“Success!”, response);
}, function(error) {
console.error(“Failed!”, error);
});
點擊這裏查看代碼。現在我們不需要手敲 XMLHttpRequest 就可以直接發起 HTTP 請求,這樣感覺好多了,能少看一次這個狂駝峯命名的 XMLHttpRequest 我就多快樂一點。

鏈式調用
“then”的故事還沒完,你可以把這些“then”串聯起來修改結果或者添加進行更多異步操作。

值的處理

你可以對結果做些修改然後返回一個新值:

var promise = new Promise(function(resolve, reject) {
resolve(1);
});
promise.then(function(val) {
console.log(val); // 1
return val + 2;}).then(function(val) {
console.log(val); // 3
});
回到前面的代碼:

get(‘story.json’).then(function(response) {
console.log(“Success!”, response);
});
收到的響應是一個純文本的 JSON,我們可以修改 get 函數,設置 responseType爲 JSON 來指定服務器響應格式,也可以在 Promise 的世界裏搞定這個問題:

get(‘story.json’).then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log(“Yey JSON!”, response);
});
既然 JSON.parse 只接收一個參數,並返回轉換後的結果,我們還可以再精簡一下:

get(‘story.json’).then(JSON.parse).then(function(response) {
console.log(“Yey JSON!”, response);
});
點擊這裏查看代碼運行頁面,打開控制檯查看輸出結果。 事實上,我們可以把getJSON 函數寫得超級簡單:

function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON 會返回一個獲取 JSON 並加以解析的 Promise。

隊列的異步操作

你也可以把“then”串聯起來依次執行異步操作。

當你從“then”的回調函數返回的時候,這裏有點小魔法。如果你返回一個值,它就會被傳給下一個“then”的回調;而如果你返回一個“類 Promise”的對象,則下一個“then”就會等待這個 Promise 明確結束(成功/失敗)纔會執行。例如:

getJSON(‘story.json’).then(function(story) {
return getJSON(story.chapterUrls[0]);}).then(function(chapter1) {
console.log(“Got chapter 1!”, chapter1);
});
這裏我們發起一個對“story.json”的異步請求,返回給我們更多 URL,然後我們會請求其中的第一個。Promise 開始首次顯現出相較事件回調的優越性了。你甚至可以寫一個抓取章節內容的獨立函數:

var storyPromise;function getChapter(i) {
storyPromise = storyPromise || getJSON(‘story.json’);

return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})}// 用起來非常簡單:getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);}).then(function(chapter) {
console.log(chapter);});
我們一開始並不加載 story.json,直到第一次 getChapter,而以後每次getChapter 的時候都可以重用已經加載完成的 story Promise,所以 story.json 只需要請求一次。Promise 好棒!

錯誤處理
前面已經看到,“then”接受兩個參數,一個處理成功,一個處理失敗(或者說肯定和否定,按 Promise 術語):

get(‘story.json’).then(function(response) {
console.log(“Success!”, response);}, function(error) {
console.log(“Failed!”, error);});
你還可以使用“catch”:

get(‘story.json’).then(function(response) {
console.log(“Success!”, response);}).catch(function(error) {
console.log(“Failed!”, error);});
這裏的 catch 並無任何特殊之處,只是 then(undefined, func) 的語法糖衣,更直觀一點而已。注意上面兩段代碼的行爲不僅相同,後者相當於:

get(‘story.json’).then(function(response) {
console.log(“Success!”, response);}).then(undefined, function(error) {
console.log(“Failed!”, error);});
差異不大,但意義非凡。Promise 被否定之後會跳轉到之後第一個配置了否定回調的 then(或 catch,一樣的)。對於 then(func1, func2) 來說,必會調用 func1或 func2 之一,但絕不會兩個都調用。而 then(func1).catch(func2) 這樣,如果 func1 返回否定的話 func2 也會被調用,因爲他們是鏈式調用中獨立的兩個步驟。看下面這段代碼:

asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log(“Don’t worry about it”);
}).then(function() {
console.log(“All done!”);
});
這段流程非常像 JavaScript 的 try/catch 組合,“try”代碼塊中發生的錯誤會立即跳轉到“catch”代碼塊。這是上面那段代碼的流程圖(我最愛流程圖了):

wKiom1TgaJ2AwNt2AAI38vV7Gcw967.jpg

綠線是肯定的 Promise 流程,紅線是否定的 Promise 流程。

JavaScript 異常和 Promise

Promise 的否定回調可以由 Promise.reject() 觸發,也可以由構造器回調中拋出的錯誤觸發:

var jsonPromise = new Promise(function(resolve, reject) {
// 如果數據格式不對的話 JSON.parse 會拋出錯誤
// 可以作爲隱性的否定結果:
resolve(JSON.parse(“This ain’t JSON”));
});
jsonPromise.then(function(data) {
// 永遠不會發生:
console.log(“It worked!”, data);
}).catch(function(err) {
// 這纔是真相:
console.log(“It failed!”, err);
});
這意味着你可以把所有 Promise 相關操作都放在它的構造函數回調中進行,這樣發生任何錯誤都能捕捉到並且觸發 Promise 否定。

“then”回調中拋出的錯誤也一樣:

get(‘/’).then(JSON.parse).then(function() {
// This never happens, ‘/’ is an HTML page, not JSON
// so JSON.parse throws
console.log(“It worked!”, data);
}).catch(function(err) {
// Instead, this happens:
console.log(“It failed!”, err);
});
實踐錯誤處理

回到我們的故事和章節,我們用 catch 來捕捉錯誤並顯示給用戶:

getJSON(‘story.json’).then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
addHtmlToPage(chapter1.html);
}).catch(function() {
addTextToPage(“Failed to show chapter”);
}).then(function() {
document.querySelector(‘.spinner’).style.display = ‘none’;
});
如果請求 story.chapterUrls[0] 失敗(http 500 或者用戶掉線什麼的)了,它會跳過之後所有針對成功的回調,包括 getJSON 中將響應解析爲 JSON 的回調,和這裏把第一張內容添加到頁面裏的回調。JavaScript 的執行會進入 catch 回調。結果就是前面任何章節請求出錯,頁面上都會顯示“Failed to show chapter”。

和 JavaScript 的 try/catch 一樣,捕捉到錯誤之後,接下來的代碼會繼續執行,按計劃把加載指示器給停掉。上面的代碼就是下面這段的非阻塞異步版:

try {
var story = getJSONSync(‘story.json’);
var chapter1 = getJSONSync(story.chapterUrls[0]);
addHtmlToPage(chapter1.html);
}catch (e) {
addTextToPage(“Failed to show chapter”);
}
document.querySelector(‘.spinner’).style.display = ‘none’;
如果只是要捕捉異常做記錄輸出而不打算在用戶界面上對錯誤進行反饋的話,只要拋出 Error 就行了,這一步可以放在 getJSON 中:

function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log(“getJSON failed for”, url, err);
throw err;
});}
現在我們已經搞定第一章了,接下來搞定全部章節。

並行和串行 —— 魚與熊掌兼得
異步的思維方式並不符合直覺,如果你覺得起步困難,那就試試先寫個同步的方法,就像這個:

try {
var story = getJSONSync(‘story.json’);
addHtmlToPage(story.heading);

story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});

addTextToPage(“All done”);}catch (err) {
addTextToPage(“Argh, broken: ” + err.message);
}
document.querySelector(‘.spinner’).style.display = ‘none’;
它執行起來完全正常(查看示例)!不過它是同步的,在加載內容時會卡住整個瀏覽器。要讓它異步工作的話,我們用 then 把它們一個接一個串起來:

getJSON(‘story.json’).then(function(story) {
addHtmlToPage(story.heading);

// TODO: 獲取並顯示 story.chapterUrls 中的每個 url
}).then(function() {
// 全部完成啦!
addTextToPage(“All done”);}).catch(function(err) {
// 如果過程中有任何不對勁的地方
addTextToPage(“Argh, broken: ” + err.message);
}).then(function() {
// 無論如何要把 spinner 隱藏掉
document.querySelector(‘.spinner’).style.display = ‘none’;
});
那麼我們如何遍歷章節的 URL 並且依次請求?這樣是不行的:

story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
});
“forEach” 沒有對異步操作的支持,所以我們的故事章節會按照它們加載完成的順序顯示,基本上《低俗小說》就是這麼寫出來的。我們不寫低俗小說,所以得修正它:

創建序列

我們要把章節 URL 數組轉換成 Promise 的序列,還是用 then:

// 從一個完成狀態的 Promise 開始
var sequence = Promise.resolve();
// 遍歷所有章節的 url
story.chapterUrls.forEach(function(chapterUrl) {
// 從 sequence 開始把操作接龍起來
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
});
這是我們第一次用到 Promise.resolve,它會依據你傳的任何值返回一個 Promise。如果你傳給它一個類 Promise 對象(帶有 then 方法),它會生成一個帶有同樣肯定/否定回調的 Promise,基本上就是克隆。如果傳給它任何別的值,如Promise.resolve(‘Hello’),它會創建一個以這個值爲完成結果的 Promise,如果不傳任何值,則以 undefined 爲完成結果。

還有一個對應的 Promise.reject(val),會創建以你傳入的參數(或 undefined)爲否定結果的 Promise。

我們可以用 array.reduce 精簡一下上面的代碼:

// 遍歷所有章節的 url
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// 從 sequence 開始把操作接龍起來
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
它和前面的例子功能一樣,但是不需要顯式聲明 sequence 變量。reduce 回調會依次應用在每個數組元素上,第一輪裏的“sequence”是 Promise.resolve(),之後的調用裏“sequence”就是上次函數執行的的結果。array.reduce 非常適合用於把一組值歸併處理爲一個值,正是我們現在對 Promise 的用法。

彙總上面的代碼

getJSON(‘story.json’).then(function(story) {
addHtmlToPage(story.heading);

return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// 當前一個章節的 Promise 完成之後……
return sequence.then(function() {
// ……獲取下一章
return getJSON(chapterUrl);
}).then(function(chapter) {
// 並添加到頁面
addHtmlToPage(chapter.html);
});
}, Promise.resolve());}).then(function() {
// 現在全部完成了!
addTextToPage(“All done”);}).catch(function(err) {
// 如果過程中發生任何錯誤
addTextToPage(“Argh, broken: ” + err.message);
}).then(function() {
// 保證 spinner 最終會隱藏
document.querySelector(‘.spinner’).style.display = ‘none’;

});
查看代碼運行示例,前面的同步代碼改造成了完全異步的版本。我們還可以更進一步。

瀏覽器很擅長同時加載多個文件,我們這種一個接一個下載章節的方法非常低效率。我們希望同時下載所有章節,全部完成後一次搞定,正好就有這麼個 API:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//…
});
Promise.all 接受一個 Promise 數組爲參數,創建一個當所有 Promise 都完成之後就完成的 Promise,它的完成結果是一個數組,包含了所有先前傳入的那些 Promise 的完成結果,順序和將它們傳入的數組順序一致。

getJSON(‘story.json’).then(function(story) {
addHtmlToPage(story.heading);

// 接受一個 Promise 數組並等待他們全部完成
return Promise.all(
// 把章節 URL 數組轉換成對應的 Promise 數組
story.chapterUrls.map(getJSON)
);}).then(function(chapters) {
// 現在我們有了順序的章節 JSON,遍歷它們……
chapters.forEach(function(chapter) {
// ……並添加到頁面中
addHtmlToPage(chapter.html);
});
addTextToPage(“All done”);}).catch(function(err) {
// 捕獲過程中的任何錯誤
addTextToPage(“Argh, broken: ” + err.message);}).then(function() {
document.querySelector(‘.spinner’).style.display = ‘none’;
});
根據連接狀況,改進的代碼會比順序加載方式提速數秒(查看示例),甚至代碼行數也更少。章節加載完成的順序不確定,但它們顯示在頁面上的順序準確無誤。

然而這樣還是有提高空間。當第一章內容加載完畢我們可以立即填進頁面,這樣用戶可以在其他加載任務尚未完成之前就開始閱讀;當第三章到達的時候我們不動聲色,第二章也到達之後我們再把第二章和第三章內容填入頁面,以此類推。

爲了達到這樣的效果,我們同時請求所有的章節內容,然後創建一個序列依次將其填入頁面:

getJSON(‘story.json’).then(function(story) {
addHtmlToPage(story.heading);

// 把章節 URL 數組轉換成對應的 Promise 數組
// 這樣就可以並行加載它們
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// 使用 reduce 把這些 Promise 接龍
// 以及將章節內容添加到頁面
return sequence.then(function() {
// 等待當前 sequence 中所有章節和本章節的數據到達
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());}).then(function() {
addTextToPage(“All done”);}).catch(function(err) {
// 捕獲過程中的任何錯誤
addTextToPage(“Argh, broken: ” + err.message);}).then(function() {
document.querySelector(‘.spinner’).style.display = ‘none’;
});
哈哈(查看示例),魚與熊掌兼得!加載所有內容的時間未變,但用戶可以更早看到第一章。

這個小例子中各部分章節加載差不多同時完成,逐章顯示的策略在章節內容很多的時候優勢將會更加顯著。

上面的代碼如果用 Node.js 風格的回調或者事件機制實現的話代碼量大約要翻一倍,更重要的是可讀性也不如此例。然而,Promise 的厲害不止於此,和其他 ES6 的新功能結合起來還能更加高效……

附贈章節:Promise 和 Generator
接下來的內容涉及到一大堆 ES6 的新特性,不過對於現在應用 Promise 來說並非必須,把它當作接下來的第二部豪華續集的預告片來看就好了。

ES6 還給我們帶來了 Generator,允許函數在特定地方像 return 一樣退出,但是稍後又能恢復到這個位置和狀態上繼續執行:

function *addGenerator() {
var i = 0;
while (true) {
i += yield i;
}}
注意函數名前的星號,這表示該函數是一個 Generator。關鍵字 yield 標記了暫停/繼續的位置,使用方法像這樣:

var adder = addGenerator();
adder.next().value; // 0
adder.next(5).value; // 5
adder.next(5).value; // 10
adder.next(5).value; // 15
adder.next(50).value; // 65
這對 Promise 有什麼用呢?你可以用這種暫停/繼續的機制寫出來和同步代碼看上去差不多(理解起來也一樣簡單)的代碼。下面是一個輔助函數(helper function),我們在 yield 位置等待 Promise 完成:

function spawn(generatorFunc) {
function continuer(verb, arg) {
var result;
try {
result = generatorverb;
} catch (err) {
return Promise.reject(err);
}
if (result.done) {
return result.value;
} else {
return Promise.resolve(result.value).then(onFulfilled, onRejected);
}
}
var generator = generatorFunc();
var onFulfilled = continuer.bind(continuer, “next”);
var onRejected = continuer.bind(continuer, “throw”);
return onFulfilled();
}
這段代碼原樣拷貝自 Q,只是改成 JavaScript Promise 的 API。把我們前面的最終方案和 ES6 最新特性結合在一起之後:

spawn(function *() {
try {
// ‘yield’ 會執行一個異步的等待,返回這個 Promise 的結果
let story = yield getJSON(‘story.json’);
addHtmlToPage(story.heading);

// 把章節 URL 數組轉換成對應的 Promise 數組
// 以便並行加載
let chapterPromises = story.chapterUrls.map(getJSON);

for (let chapterPromise of chapterPromises) {
  // 等待每個章節加載完成,將其添加至頁面
  let chapter = yield chapterPromise;
  addHtmlToPage(chapter.html);
}

addTextToPage("All done");

}
catch (err) {
// try/catch 即可。否定的 Promise 會在這裏拋出。
addTextToPage(“Argh, broken: ” + err.message);
}
document.querySelector(‘.spinner’).style.display = ‘none’;
});
功能完全一樣,讀起來要簡單得多。這個例子目前可以在 Chrome Canary 中運行(查看示例),不過你得先到 about:flags 中開啓 Enable experimental JavaScript 選項。

這裏用到了一堆 ES6 的新語法:Promise、Generator、let、for-of。當我們把yield 應用在一個 Promise 上,spawn 輔助函數會等待 Promise 完成,然後才返回最終的值。如果 Promise 給出否定結果,spawn 中的 yield 則會拋出一個異常,我們可以用 try/catch 捕捉到。這樣寫異步代碼真是超級簡單!

Promise API 參考
除非額外註明,Chrome、Opera 和 Firefox(nightly)均支持下列所有方法。這個 polyfill 則在所有瀏覽器內實現了同樣的接口。

靜態方法

Promise.resolve(promise);
返回一個 Promise(當且僅當 promise.constructor == Promise)
Promise.resolve(thenable);
從 thenable 對象創建一個新的 Promise。一個 thenable(類 Promise)對象是一個帶有“then”方法的對象。
Promise.resolve(obj);
創建一個以 obj 爲肯定結果的 Promise。
Promise.reject(obj);
創建一個以 obj 爲否定結果的 Promise。爲了一致性和調試便利(如堆棧追蹤),obj 應該是一個 Error 實例對象。
Promise.all(array);
創建一個 Promise,當且僅當傳入數組中的所有 Promise 都肯定之後才肯定,如果遇到數組中的任何一個 Promise 以否定結束,則拋出否定結果。每個數組元素都會首先經過 Promise.resolve,所以數組可以包含類 Promise 對象或者其他對象。肯定結果是一個數組,包含傳入數組中每個 Promise 的肯定結果(且保持順序);否定結果是傳入數組中第一個遇到的否定結果。
Promise.race(array);
創建一個 Promise,當數組中的任意對象肯定時將其結果作爲肯定結束,或者當數組中任意對象否定時將其結果作爲否定結束。
備註:我不大確定這個接口是否有用,我更傾向於一個 Promise.all 的對立方法,僅當所有數組元素全部給出否定的時候才拋出否定結果

構造器

new Promise(function(resolve, reject) {});
resolve(thenable)
你的 Promise 將會根據這個 “thenable” 對象的結果而返回肯定/否定結果
resolve(obj)
你的 Promise 將會以 obj 作爲肯定結果完成
reject(obj)
你的 Promise 將會以 obj 作爲否定結果完成。出於一致性和調試(如棧追蹤)方便,obj 應該是一個 Error 對象的實例。構造器的回調函數中拋出的錯誤會被立即傳遞給 reject()。
實例方法

promise.then(onFulfilled, onRejected)
當 promise 以肯定結束時會調用 onFulfilled。 當 promise 以否定結束時會調用onRejected。 這兩個參數都是可選的,當任意一個未定義時,對它的調用會跳轉到 then 鏈的下一個 onFulfilled/onRejected 上。 這兩個回調函數均只接受一個參數,肯定結果或者否定原因。 當 Promise.resolve 肯定結束之後,then 會返回一個新的 Promise,這個 Promise 相當於你從 onFulfilled/onRejected 中返回的值。如果回調中拋出任何錯誤,返回的 Promise 也會以此錯誤作爲否定結果結束。
promise.catch(onRejected)
promise.then(undefined, onRejected) 的語法糖。
非常感謝 Anne van Kesteren,Domenic Denicola,Tom Ashworth,Remy Sharp,Addy Osmani,Arthur Evans 以及 and Yutaka Hirano 對本文做出的貢獻(審閱/糾錯或建議)。

發佈了60 篇原創文章 · 獲贊 22 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章