Promise 可以簡化延遲和非同步運算。承諾代表尚未完成的作業。
開發人員可以準備進入 網站開發應用程式。
[鼓聲響開始]
Promise 已加入 JavaScript!
[煙火爆炸、從空中閃耀的紙雨,天生大爆炸]
目前您的所屬類別如下:
- 你周圍的人都是歡呼,但你不確定那是什麼 介紹生成式 AI 模型也許你根本不知道會有什麼不過,無論內部 IP 位址為何 DNS 名稱始終會指向特定的執行個體你聳肩 閃亮光滑的紙張重量落在肩膀上。如果有,請不要 別擔心,我花了很多時間研究自己為何該關心這件事 內容您可能會想從開始著手。
- 你打出空中!剛剛好嗎?你曾使用這些 Promise 但您會擔心所有實作的 API 略有不同。 官方 JavaScript 版本的 API 為何?不妨先從 建立術語。
- 您深知這件事情,也對那些勇往直前, 對他們來說是很重要的花點時間好好感受自己的優越表現 然後直接參閱 API 參考資料。
瀏覽器支援和 polyfill
為缺乏完整承諾的瀏覽器 或對其他瀏覽器和 Node.js 提供保證 polyfill (2K gzip 壓縮)。
有什麼煩惱嗎?
JavaScript 是單一執行緒,意味著無法在 之後就必須執行一個在瀏覽器中,使用 JavaScript 這個執行緒與其他平台不同 。但 JavaScript 通常和繪製作業位於相同的佇列中,並進行更新 樣式、處理使用者動作 (例如醒目顯示文字和互動) 和表單控制項)。發生上述任何一項活動時,其他活動會延遲顯示。
人在說,「多執行緒」。您可以用多指輸入文字 就能同時發起對話唯一的封鎖方式 我們需要處理的功能是打噴嚏,而目前所有活動都必須 在打噴嚏期間遭到停權。實在是很令人困擾 尤其是在開車或嘗試保留對話時不要 就像寫程式那樣
您可能已運用事件和回呼解決這個問題。以下為這類活動:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
這個結果完全沒有洩漏。取得圖片,新增幾個事件監聽器,接著 JavaScript 可能會停止執行,直到呼叫其中一個接聽程式為止。
遺憾的是,在上述範例中,事件可能發生 因此,我們需要使用 「完整」圖片屬性:
var img1 = document.querySelector('.img-1');
function loaded() {
// woo yey image loaded
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// argh everything's broken
});
無法在在我們有機會監聽前擷取出錯誤的圖片 them;可惜的是, DOM 無法達成此目的。此外,這是 正在載入一張圖片。而我們單憑藉這個方式得知 已載入 張圖片。
活動有時不見得
活動適合在同一時間發生的重複活動
物件—keyup
、touchstart
等。您並不在乎這些事件
附加監聽器之前發生的事件但以這個模型來說
非同步成功/失敗的功能,理想情況下,不妨採用以下做法:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
這樣的命名方式更好,但名稱更貼合標準。如果 HTML 圖片元素包含 「準備好」方法,這樣我們就可以:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
基本上,保證有點像事件監聽器,除了:
- 一個承諾只能成功或出現一次。無法成功或失敗兩次 兩者都不會切換為失敗或失敗,反之亦然。
- 如果承諾成功或失敗,且您之後新增成功/失敗 回呼的情況下,即使事件產生 放在較早的位置
這對非同步成功/失敗來說非常實用,因為 希望瞭解產品的確切上市時間 回應結果
承諾術語
Domenic Denicola 證明閱讀初稿 並將我評為「F」這個術語他將我成為注意力 強制我複製 州與命運 這也寫了一封信念給爸媽。儘管如此 混合使用相同術語,但基本知識如下:
例如:
- fulfill - 與承諾內容相關的動作成功
- rejected:無法執行與承諾相關的動作
- 待處理 - 尚未出貨或遭到拒絕
- settled - 已完成或遭到拒絕
規格
同樣使用 thenable 字詞描述類似承諾的物件
其中含有 then
方法這個字詞提醒我參加前英格蘭足球賽
管理員 Terry Venables
我會盡可能減少使用它。
Promise 導入 JavaScript!
Promise 出現在程式庫中已有一段時間,例如:
上述方法和 JavaScript 都承諾具有相同的標準化行為。 稱為 Promises/A+。如果 您是 jQuery 使用者 延遲。不過 延遲不符合 Promise/A+ 規範,因此違反規定 有些許不同、較不實用, 請小心jQuery 也 Promise 類型,但這只是 但一樣的問題
雖然保證導入作業遵循標準化行為, 整體 API 的差異在 API 中,JavaScript 的保證與 RSVP.js 很類似。 以下說明建立承諾的方式:
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
承諾的建構函式會採用一個引數,即含有兩個參數的回呼。 解決並拒絕在回呼中執行操作,可能是非同步,然後呼叫 如果一切運作正常,就表示呼叫拒接。
與純舊 JavaScript 中的 throw
一樣,這是自訂功能,但並非必要
拒絕錯誤。Error 物件的優點是會擷取
堆疊追蹤,讓偵錯工具更加實用。
以下說明您如何運用這項承諾:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()
會使用兩個引數,一個成功案例的回呼
以供失敗案例使用兩者皆為選用,因此您可以新增
僅限成功或失敗案例
在 DOM 中啟動的 JavaScript 承諾為「Futures」,並重新命名為「Promises」。 最後移到 JavaScript使用 JavaScript 而非 DOM 很適合用於非瀏覽器的 JS 環境,例如 Node.js (是否在核心 API 中使用 Node.js,也是其他問題)。
雖然是 JavaScript 功能,但是 DOM 無法使用。於 事實上,所有具備非同步成功/失敗方法的新 DOM API 都會使用承諾。 這個問題已經發生在 配額管理、 字型載入事件、 ServiceWorker、 Web MIDI、 串流等。
與其他程式庫的相容性
JavaScript 承諾 API 會將包含 then()
方法的所有內容視為
類似承諾使用 (或以 thenable
提供承諾,表示「嘆氣」),因此如果您使用程式庫
能夠傳回 Q 承諾,別擔心,使用新的
JavaScript 保證。
不過如我所說,jQuery 的「延遲」有點不實用。 謝天謝地,各位可以做出標準承諾,這是很值得的 越快越好:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
這裡,jQuery 的 $.ajax
會傳回 Deferred。它採用 then()
方法
Promise.resolve()
可以將其轉換為 JavaScript 承諾。不過
有時可能會延遲將多個引數傳遞至其回呼,例如:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
然而,JS 可保證只會忽略第一個細節:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
幸好這通常是您想要的,或至少開放您存取 你想要什麼另請注意,jQuery 不會遵循 將錯誤物件傳送至拒絕項目。
簡化複雜的非同步程式碼
現在來編寫一些程式碼假設我們想:
- 啟動旋轉圖示來表示載入中
- 擷取故事的部分 JSON,讓我們取得每個章節的標題和網址
- 新增頁面標題
- 擷取各章節
- 將故事新增至頁面
- 停止旋轉圖示
...而且也告知使用者註冊過程是否發生問題。我們會 停止旋轉圖示,不然就會一直旋轉, 當機並當機到其他 UI 中
當然,你不會用 JavaScript 傳達故事 以 HTML 格式放送 但這種模式在處理 API 時很常見:多重資料 擷取作業完成後再執行其他操作
首先,我們要從網路擷取資料:
證明 XMLHttpRequest
如果可以反向操作,系統會將舊版 API 更新為使用承諾
。「XMLHttpRequest
」是最佳候選人,但在平均值中
我們來撰寫一個簡單的函式來發出 GET 要求:
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
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 函式來使用 JSON
responseType
、
但我們也可以在承諾中解決該問題:
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 格式回應
將非同步動作排入佇列
您也可以鏈結 then
,依序執行非同步動作。
從 then()
回呼傳回內容時,有點神奇的作用。
如果傳回值,系統會使用該值呼叫下一個 then()
。不過
如果您傳回類似承諾的內容,下一個 then()
就會等候該結果,且
只有在保證和解結果 (成功/失敗) 時才會呼叫。例如:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
我們在此向 story.json
發出非同步要求,以取得一組
收到要求的網址,然後我們會要求第一個網址。適用於保證
確實會從簡單的回呼模式中脫穎而出
你甚至可以建立捷徑方法,以便取得章節:
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
系統會在呼叫 getChapter
前下載 story.json
,但接下來會下載
我們重複使用故事承諾,因此 story.json
次getChapter
只會擷取一次哇,我很厲害!
處理錯誤
如先前所述,then()
會採用兩個引數,一個代表成功,一個
失敗 (或履行與拒絕,在保證詞語中):
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
拒絕,就會呼叫此方法,因為在鏈結中屬於獨立步驟。搭乘
包括:
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 非常類似,
在「嘗試」內發生請立即前往 catch()
區塊接著來介紹
顯示為流程圖 (因為我喜歡流程圖):
遵照藍色線條表示能履行的承諾,否則請遵照紅色線條表示 拒絕。
JavaScript 例外狀況和保證
拒絕會在承諾中明確遭拒 (但暗示性) 發生 建構函式回呼中擲回錯誤時的結果:
var jsonPromise = new Promise(function(resolve, reject) {
// JSON.parse throws an error if you feed it some
// invalid JSON, so this implicitly rejects:
resolve(JSON.parse("This ain't JSON"));
});
jsonPromise.then(function(data) {
// This never happens:
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
也就是說,在 因此能自動擷取錯誤 就會遭到拒絕
對 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);
})
實務中的錯誤處理方式
我們可以透過故事和章節,用擷取的方式向使用者顯示錯誤訊息:
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,而且也會略過
這個回呼函式會將 chapter1.html 新增至網頁。而是改採漁獲
回呼。這會導致系統顯示「無法顯示章節」就會加進頁面
先前的任何動作都失敗。
就像 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'
你可能只想將 catch()
用於記錄,不必復原
錯誤。要這麼做,只要再次擲回錯誤即可。我們可以
我們的 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: for each url in story.chapterUrls, fetch & display
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
但該如何循環播放特定章節網址,並依序擷取內容?這個 錯誤情形:
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
forEach
無法以非同步方式辨識,因此章節會以任何順序顯示
這基本上就是《Pulp 很多》的寫作方式這並不是
青春喜劇,我們一起來修正。
建立序列
我們希望將 chapterUrls
陣列轉換為一系列承諾。我們可以使用 then()
來完成這項操作:
// Start off with a promise that always resolves
var sequence = Promise.resolve();
// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
// Add these actions to the end of the sequence
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
})
這是我們第一次看到 Promise.resolve()
,它會建立
而是能夠解析您所指定的價值如果將
Promise
的例項會直接傳回 (注意:這是
例如某些實作項目尚未遵循的規格)。如果發生以下情況:
傳遞類似承諾的內容 (包含 then()
方法) 便會建立
以相同方式執行/拒絕的正版 Promise
。如果通過
任何其他值 (例如Promise.resolve('Hello')
後,系統會建立
保證可以達到該價值如果直接呼叫函式沒有價值
工作,便會使用「未定義」來執行。
而且 Promise.reject(val)
會建立拒絕接受規則的承諾
您指定 (或未定義) 的值
您可以使用
array.reduce
:
// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Add these actions to the end of the sequence
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
這個做法與上一個範例相同,但不需要個別
「序列」變數。陣列中的每個項目都會呼叫減少回呼。
「序列」第一次使用 Promise.resolve()
,但在其餘時間
呼叫「sequence」是上一次呼叫傳回的任何值。array.reduce
這個方法非常適合用來將陣列輸出為單一值
確實是個承諾
總結:
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter's promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
這就是同步版本的同步版本但我們也可以 目前的頁面下載作業如下:
瀏覽器很擅長同時下載多個項目, 逐一下載章節,進一步提升收益。我們的目標是 同時下載它們,到貨後再進行處理。 幸好有 API 可以:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
會採納一系列承諾,並提出能履行的承諾
但當所有工作都順利完成時您會得到結果陣列 (無論
保證範圍內)。
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened so far
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
視網路連線狀況而定 與逐一載入 這比我們第一次嘗試的程式碼少章節可任意下載 但會以正確順序顯示。
不過,我們依然可以改善使用者感知的成效。第一章抵達時 應該將程式碼新增至網頁。如此一來,使用者就能在其餘時間之前開始閱讀 許多章節都來了。第三章抵達時,我們不會將它新增到 因為使用者可能並不瞭解到缺少第 2 章進入第二章時 我們可以加入 2 個和 3 個章節,以此類推
為此,我們會同時擷取所有章節的 JSON,然後建立 新增到文件中:
getJSON('story.json')
.then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download in parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence
.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
結果顯示,兩人都大功告成!放送所需時間相同 但使用者還是能更快取得內容的第一部分。
在這個簡單的範例中,所有章節都在相同時間抵達,但 一次顯示一個廣告的好處,將大幅增加 也會透過這個系列
使用 Node.js 樣式回呼或 附近的活動 但更重要的是不易於遵循不過, 與其他 ES6 功能結合時, 變得更加簡單
額外獎勵:擴充功能
我原本撰寫了這篇文章,因此 Promise 使用的功能已擴大 。自 Chrome 55 版起,非同步函式允許以承諾為基準的程式碼 撰寫類似同步的寫入,但不會封鎖主執行緒。你可以 詳情請參閱非同步函式說明文章。還有 廣泛支援主要瀏覽器中的 Promise 和 async 函式。 您可以在 MDN 的 Promise 和 Async 函式 參照。
感謝 Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp 和 Addy Osmani、Arthur Evans 和 Yutaka Hirano 完成校對程序 修正/建議
此外,感謝 Mathias Bynens 貢獻一己之力 更新應用程式 文章的部分