翻譯服務

翻譯服務

學習練習

簡介

Promise物件代表一個非同步操作的最終完成(或失敗),以及它所產生的值。

Note

對很多人來說,這是個很難的主題,尤其是當你熟悉的程式語言是完全_同步_的時候。 如果你感到吃不消,或者想更深入了解並行與平行,可以觀看(透過 go.dev)或直接透過 vimeo 觀看,並閱讀投影片,這些都出自精彩的演講「Concurrency is not parallelism」。

Promise 的生命週期

Promise有三種狀態:

  1. pending
  2. fulfilled
  3. rejected

建立時,Promise 處於 pending 狀態。 在未來的某個時間點,它可能會_解決_或_拒絕_。 Promise 一旦變成 resolved 或 rejected 一次,就永遠無法再變成 resolved 或 rejected,它的狀態也無法改變。

換句話說:

  1. 處於 pending 時,Promise:
    • 可以轉變成 fulfilled 或 rejected 狀態。
  2. 處於 fulfilled 時,Promise:
    • 不得轉變成任何其他狀態。
    • 必須有一個值,而且這個值不得改變。
  3. 處於 rejected 時,Promise:
    • 不得轉變成任何其他狀態。
    • 必須有一個原因,而且這個原因不得改變。

解決 Promise

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

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 都是「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);
  • 大約有一半的情況,這會在大約 1000 毫秒後印出"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,可以再解決或拒絕。

catch

有時候你會想捕捉錯誤,只在原本的 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";

finally

有時候你會想在某個 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 completed
  • 當randomNumber是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來拒絕。

Note

如果串接 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.

⚠ 警告!⚠

Caution

當有 request 進來時,API 會透過瞬間傳送各個翻譯員來施展它的魔法。 這是個非常昂貴的操作,所以當翻譯已經可用時,不應該呼叫它。 遺憾的是,不是每個人都會讀手冊,所以有一套機制會把不良行為者踢出去。

如果對已經可用的 text 呼叫 api.request,API 會針對這次呼叫,以及之後的每一次呼叫,拋出 AbusiveClientError。 請確保當某個東西已經被翻譯過時,你絕對不要請求翻譯。

1. 取得翻譯,忽略品質

免費服務只提供目前存在於 API 儲存 中的翻譯。

實作一個方法 free(text),為免費會員提供已經存在於 API 儲存 中的翻譯。忽略品質,並將 API 拋出的任何錯誤往外傳遞。

  • 如果可以取得翻譯,就回傳該翻譯,無論其品質為何
  • 將翻譯 API 的任何錯誤往外傳遞
  • 使用 api.fetch 方法(api.fetch 會回傳一個 promise)
service.free('jIyaj');
// => Promise<...> resolves "I understand."

service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")

2. 批次取得翻譯,全有或全無

為免費會員實作一個方法 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()

3. 請求翻譯,最多重試 2 次

實作一個付費使用者方法 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

4. 取得翻譯,檢查品質,或請求翻譯

實作一個付費使用者方法 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?"

注意

Note

'arlogh Qoylu'pu'? 的正確翻譯是它被聽到了多少次?。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
JavaScript Exercism

準備好開始 翻譯服務 了嗎?

註冊 Exercism,透過 37 個概念159 個練習 和真人引導來學習並精通 JavaScript,全部免費。