Promise物件代表一個非同步操作的最終完成(或失敗),以及它所產生的值。
對很多人來說,這是個很難的主題,尤其是當你熟悉的程式語言是完全_同步_的時候。 如果你感到吃不消,或者想更深入了解並行與平行,可以觀看(透過 go.dev)或直接透過 vimeo 觀看,並閱讀投影片,這些都出自精彩的演講「Concurrency is not parallelism」。
Promise有三種狀態:
建立時,Promise 處於 pending 狀態。 在未來的某個時間點,它可能會_解決_或_拒絕_。 Promise 一旦變成 resolved 或 rejected 一次,就永遠無法再變成 resolved 或 rejected,它的狀態也無法改變。
換句話說:
Promise 可以用各種方式解決:
// Creates a promise that is immediately resolved
Promise.resolve(value);
// Creates a promise that is immediately resolved
new Promise((resolve) => {
resolve(value);
});
// Chaining a promise leads to a resolved promise
somePromise.then(() => {
// ...
return value;
});
在以上的例子中,value可以是_任何東西_,包括錯誤、undefined、null或其他 Promise。
通常你會想用一個不是錯誤的值來解決。
Promise 可以用各種方式拒絕:
// Creates a promise that is immediately rejected
Promise.reject(reason)
// Creates a promise that is immediately rejected
new Promise((_, reject) {
reject(reason)
})
// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
// ...
throw reason
})
在以上的例子中,reason可以是_任何東西_,包括錯誤、undefined或null。
通常你會想用一個錯誤來拒絕。
Promise 可以在解決或拒絕之後,用後續動作來_繼續_。
promise.then()會在promise解決後被呼叫promise.catch()會在promise拒絕後被呼叫promise.finally()會在promise解決或拒絕後被呼叫每個 Promise 都是「thenable」。
這表示有一個then函式可用,它會在原本的 Promise 解決後被執行。
以promise.then(onResolved)來說,回呼onResolved會收到原本 Promise 被解決時所帶的值。
這總是會回傳一個_新_的「串接」Promise。
在then裡回傳value會解決這個「串接」Promise。
在then裡丟出reason會拒絕這個「串接」Promise。
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
return true;
});
這會在大約 1000 毫秒後印出"Success!"。
promise1的狀態與值會是resolved和"Success!"。
promise2的狀態與值會是resolved和true。
還有第二個引數可用,會在原本的 Promise 拒絕時執行。
以promise.then(onResolved, onRejected)來說,回呼onResolved會收到原本 Promise 被解決時所帶的值,或者回呼onRejected會收到 Promise 被拒絕的原因。
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return true;
}
function shout(reason) {
console.error(reason.toUpperCase());
return false;
}
const promise2 = promise1.then(log, shout);
"Success!"。
promise1的狀態與值會是resolved和"Success!"。promise2的狀態與值會是resolved和true。"NOPE!"。
promise1的狀態與值會是rejected和Nope!。promise2的狀態與值會是resolved和false。重要的是要理解,由於生命週期的規則,當它reject時,大約 1000 毫秒後才出現的resolve會被默默忽略,因為內部狀態一旦拒絕或解決就無法再改變。
同樣重要的是要理解,從 Promise 回傳值會解決它,而丟出值則會拒絕它。
當promise1解決,而且有串接的onResolved:then(onResolved)時,那個後續結果是一個新的 Promise,可以再解決或拒絕。
當promise1拒絕,但有串接的onRejected:then(, onRejected)時,那個後續結果是一個新的 Promise,可以再解決或拒絕。
有時候你會想捕捉錯誤,只在原本的 Promise reject 時才繼續。
以promise.catch(onCatch)來說,回呼onCatch會收到原本 Promise 被拒絕的原因。
這總是會回傳一個_新_的「串接」Promise。
在catch裡回傳value會解決這個「串接」Promise。
在catch裡丟出reason會拒絕這個「串接」Promise。
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return 'done';
}
function recover(reason) {
console.error(reason.toUpperCase());
return 42;
}
const promise2 = promise1.catch(recover).then(log);
大約有一半的情況,這會在大約 1000 毫秒後印出"Success!"。
另外一半的情況,這會立刻印出42。
promise1解決,catch會被跳過並進到then,然後印出該值。
promise1的狀態與值會是resolved和"Success!"。promise2的狀態與值會是resolved和"done";promise1拒絕,catch會被執行,而它_回傳了一個值_,因此串接結果現在變成resolved,接著進到then,並印出該值。
promise1的狀態與值會是rejected和"Nope!"。promise2的狀態與值會是resolved和"done";有時候你會想在某個 Promise 結束後執行程式碼,不論它最後是解決還是拒絕。
以promise.finally(onSettled)來說,回呼onSettled不會收到任何東西。
這總是會回傳一個_新_的「串接」Promise。
在finally裡回傳value會複製原本 Promise 的狀態與值,並忽略這個value。
在finally裡丟出reason會拒絕這個「串接」Promise,並覆寫原本 Promise 的任何狀態與值或原因。
各種方法一起使用:
const myPromise = new Promise(function (resolve, reject) {
const sampleData = [2, 4, 6, 8];
const randomNumber = Math.round(Math.random() * 5);
if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('Sampling did not result in a sample');
}
});
const finalPromise = myPromise
.then(function (sampled) {
// If the random number was 0, 1, 2, or 3, this will be
// reached and the number 2, 4, 6, or 8 will be logged.
console.log(`Sampled data: ${sampled}`);
return 'yay';
})
.catch(function (reason) {
// If the random number was 4 or 5, this will be reached and
// reason will be "An error occurred". The entire chain will
// then reject with an Error with the reason as message.
throw new Error(reason);
})
.finally(function () {
// This will always log after either the sampled data is
// logged or the error is raised.
console.log('Promise completed');
});
randomNumber是0-3時:
myPromise會以值2, 4, 6, or 8解決finalPromise會以值'yay'解決Sampled data: ...Promise completedrandomNumber是4-5時:
myPromise會以原因'Sampling did not result in a sample'拒絕finalPromise會以原因Error('Sampling did not result in a sample')拒絕Promise completed"uncaught rejected promise: Error('Sampling did not result in a sample')"紀錄如上所示,reject可以搭配字串使用,而 Promise 也可以用Error來拒絕。
如果串接 Promise 或一般用法還不清楚,MDN 上的教學是很好的資源。
在這個練習中,你將提供一個 TranslationService,它為免費會員提供基本的翻譯服務,並為付費會員提供附帶品質保證的進階翻譯。
API
你找到了一個外太空翻譯 API,它能在合理的時間內完成任何翻譯 request。你想好好利用這一點。太空翻譯員非常善變,而且討厭多餘的重複,所以他們還提供了 API 儲存 衛星,讓你可以在不打擾他們的情況下 fetch 過去的翻譯。
取得翻譯
api.fetch(text) 會從 API 儲存 取得 text 的翻譯,並回傳一個 promise,其中提供兩個值:
translation:實際的翻譯quality:以數字表示的品質如果在 API 儲存 中找不到翻譯,API 會拋出 NotAvailable 錯誤。可以使用 api.request 方法加入翻譯。如果 'text' 無法翻譯,API 會拋出 Untranslatable 錯誤。
api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })
請求翻譯
有些翻譯確實存在,但還沒被加入 API 儲存。這就是 NotAvailable(不在儲存中,但可以請求)與 Untranslatable(無法翻譯)之間的差別。
api.request(text, callback) 會請求執行 text 的翻譯,並將其加入 API 儲存。完成後,callback 函式會被呼叫。
callback 會收到 undefined:這表示翻譯成功,而且可以使用 api.fetch 方法取得。callback 會收到一個 error:這表示出了問題。這個外太空 API 是 不穩定 的,也就是說它經常失敗。如果發生這種情況,可以再次 api.request。api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
// because it was successful.
⚠ 警告!⚠
當有 request 進來時,API 會透過瞬間傳送各個翻譯員來施展它的魔法。
這是個非常昂貴的操作,所以當翻譯已經可用時,不應該呼叫它。
遺憾的是,不是每個人都會讀手冊,所以有一套機制會把不良行為者踢出去。
如果對已經可用的 text 呼叫 api.request,API 會針對這次呼叫,以及之後的每一次呼叫,拋出 AbusiveClientError。
請確保當某個東西已經被翻譯過時,你絕對不要請求翻譯。
免費服務只提供目前存在於 API 儲存 中的翻譯。
實作一個方法 free(text),為免費會員提供已經存在於 API 儲存 中的翻譯。忽略品質,並將 API 拋出的任何錯誤往外傳遞。
api.fetch 方法(api.fetch 會回傳一個 promise)service.free('jIyaj');
// => Promise<...> resolves "I understand."
service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")
為免費會員實作一個方法 batch([text, text, ...]),使用免費服務翻譯一組文字,回傳所有翻譯,或單一錯誤。
BatchIsEmpty 錯誤拒絕service.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]
service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")
service.batch([]);
// => Promise<...> rejects BatchIsEmpty()
實作一個付費使用者方法 request(text),它會_請求_將某個翻譯加入 API 儲存。如果發生失敗,這個請求應該自動重試。針對同一個請求,它應該執行不超過 3 次呼叫(別惹太空翻譯員生氣!!!)。
api.request 沒有回傳錯誤,就以 undefined 完成api.request 回傳錯誤,最多重試 2 次service.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API
實作一個付費使用者方法 premium(text, quality) 來取得翻譯。如果某個翻譯是 NotAvailable,就請求該翻譯,並在它被加入 API 儲存 之後取得它。這個方法只有在翻譯達到某個 quality 門檻時,才應該回傳該翻譯。
api.fetch 完成,就先檢查品質再完成api.fetch 拒絕,就改為_請求_該翻譯api.request 拒絕,就把錯誤往外傳遞service.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."
service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()
service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"
注意
'arlogh Qoylu'pu'? 的正確翻譯是它被聽到了多少次?。