轨道
/
JavaScript
JavaScript
/
练习
/
翻译服务
翻译服务

翻译服务

学习练习

简介

Promise对象表示一个异步操作的最终完成(或失败)及其结果值。

Note

对很多人来说,这是一个很难的话题,尤其是当你熟悉的编程语言是完全_同步_的时候。如果你感到不知所措,或者想进一步了解并发和并行,可以观看(通过 go.dev)或直接通过 vimeo 观看那场精彩的演讲“Concurrency is not parallelism”,并阅读幻灯片。

promise 的生命周期

一个Promise有三种状态:

  1. pending
  2. fulfilled
  3. rejected

创建时,promise 处于 pending 状态。在未来的某个时刻,它可能会 resolve 或 reject。一旦 promise 被 resolve 或 reject 过一次,它就再也不能被 resolve 或 reject,其状态也无法改变。

换句话说:

  1. 处于 pending 状态时,promise:
    • 可以转变为 fulfilled 或 rejected 状态。
  2. 处于 fulfilled 状态时,promise:
    • 不得转变为任何其他状态。
    • 必须有一个值,且该值不得改变。
  3. 处于 rejected 状态时,promise:
    • 不得转变为任何其他状态。
    • 必须有一个原因,且该原因不得改变。

promise 的 resolve

promise 可以通过多种方式被 resolve:

// 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。通常你希望用一个不是错误的值来 resolve。

promise 的 reject

promise 可以通过多种方式被 reject:

// 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。通常你希望用一个错误来 reject。

promise 的链式调用

promise 在 resolve 或 reject 之后,可以用一个后续操作来_继续_。

then

每个 promise 都是“thenable”的。这意味着有一个可用的then函数,它会在原始 promise resolve 之后被执行。给定promise.then(onResolved),回调onResolved会收到原始 promise 被 resolve 时所用的值。它总是会返回一个_新的_“链式”promise。

在then中返回一个value会让“链式”promise resolve。在then中抛出reason会让“链式”promise reject。

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 reject 时执行。给定promise.then(onResolved, onRejected),回调onResolved会收到原始 promise 被 resolve 时所用的值,或者回调onRejected会收到该 promise 被 reject 的原因。

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会被静默忽略,因为内部状态一旦 reject 或 resolve 就无法再改变。同样重要的是:从 promise 中返回一个值会让它 resolve,抛出一个值会让它 reject。当promise1 resolve,并且有链式的onResolved:then(onResolved)时,这段后续处理是一个新的 promise,可以 resolve 或 reject。当promise1 reject,但有链式的onRejected:then(, onRejected)时,这段后续处理是一个新的 promise,可以 resolve 或 reject。

catch

有时你想捕获错误,并且只在原始 promisereject时才继续。给定promise.catch(onCatch),回调onCatch会收到原始 promise 被 reject 的原因。它总是会返回一个_新的_“链式”promise。

在catch中返回一个value会让“链式”promise resolve。在catch中抛出reason会让“链式”promise reject。

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 resolve,catch会被跳过,流程到达then,并输出该值。
    • promise1的状态和值会是resolved和"Success!"。
    • promise2的状态和值会是resolved和"done";
  • 如果promise1 reject,catch会执行,它_返回一个值_,因此链条现在处于resolved,流程到达then,并输出该值。
    • promise1的状态和值会是rejected和"Nope!"。
    • promise2的状态和值会是resolved和"done";

finally

有时你希望在 promise settle 之后执行代码,无论这个 promise 是 resolve 还是 reject。给定promise.finally(onSettled),回调onSettled不会收到任何东西。它总是会返回一个_新的_“链式”promise。

在finally中返回一个value会复制原始 promise 的状态和值,忽略该value。在finally中抛出reason会让“链式”promise reject,并覆盖原始 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会 resolve,其值为2, 4, 6, or 8
    • finalPromise会 resolve,其值为'yay'
    • 会有两条日志:
      • Sampled data: ...
      • Promise completed
  • 在randomNumber为4-5的情况下:
    • myPromise会 reject,其原因为'Sampling did not result in a sample'
    • finalPromise会 reject,其原因为Error('Sampling did not result in a sample')
    • 会有一条日志:
      • Promise completed
      • 在某些环境中,这会产生一条"uncaught rejected promise: Error('Sampling did not result in a sample')"日志

如上所示,reject可以使用字符串,而 promise 也可以用Error来 reject。

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返回错误,最多重试两次
  • 如果重试次数用尽,就以最后收到的错误失败
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'?的正确翻译是 How many times has it been heard?。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
JavaScript Exercism

准备好开始 翻译服务 了吗?

注册 Exercism,借助 37 个概念159 个练习 和真人导师指导,学习并掌握 JavaScript,全部免费。