Promise对象表示一个异步操作的最终完成(或失败)及其结果值。
对很多人来说,这是一个很难的话题,尤其是当你熟悉的编程语言是完全_同步_的时候。如果你感到不知所措,或者想进一步了解并发和并行,可以观看(通过 go.dev)或直接通过 vimeo 观看那场精彩的演讲“Concurrency is not parallelism”,并阅读幻灯片。
一个Promise有三种状态:
创建时,promise 处于 pending 状态。在未来的某个时刻,它可能会 resolve 或 reject。一旦 promise 被 resolve 或 reject 过一次,它就再也不能被 resolve 或 reject,其状态也无法改变。
换句话说:
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:
// 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 在 resolve 或 reject 之后,可以用一个后续操作来_继续_。
promise resolve,就会调用promise.then()
promise reject,就会调用promise.catch()
promise resolve 或 reject,就会调用promise.finally()
每个 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);
"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。
有时你想捕获错误,并且只在原始 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";有时你希望在 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 completedrandomNumber为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。
如果链式调用 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返回错误,最多重试两次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'?的正确翻译是 How many times has it been heard?。