شیء Promise نشاندهندهی تکمیل نهایی (یا شکست) یک عملیات ناهمزمان و مقدار حاصل از آن است.
این موضوع برای بسیاری از افراد دشوار است، بهویژه اگر در زبانی برنامهنویسی کرده باشید که کاملاً همزمان است. اگر احساس میکنید غرق شدهاید، یا میخواهید دربارهی «همروندی» و «موازیسازی» بیشتر بدانید، سخنرانی درخشان «Concurrency is not parallelism» را تماشا کنید (از طریق go.dev) یا مستقیماً از طریق vimeo تماشا کنید و اسلایدهای آن را بخوانید.
Promise
یک Promise سه حالت دارد:
وقتی ساخته میشود، یک Promise در حالت pending است.
در نقطهای در آینده ممکن است resolve یا reject شود.
وقتی یک Promise یک بار resolve یا reject شد، دیگر هرگز نمیتواند دوباره resolve یا reject شود و حالتش هم تغییر نمیکند.
به بیان دیگر:
Promise در حالت pending:
fulfilled یا rejected منتقل شود.Promise در حالت fulfilled:
Promise در حالت rejected:
resolve کردن یک Promise
یک 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 کنید.
reject کردن یک Promise
یک 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 شد، میتوان با یک اقدام آینده آن را ادامه داد.
promise.then() زمانی فراخوانی میشود که promise resolve شودpromise.catch() زمانی فراخوانی میشود که promise reject شودpromise.finally() زمانی فراخوانی میشود که promise یا resolve شود یا reject
هر Promise «thenable» است.
یعنی یک تابع then در دسترس است که وقتی Promise اصلی resolve شد، اجرا میشود.
با promise.then(onResolved)، تابع بازگشتی onResolved مقداری را دریافت میکند که Promise اصلی با آن resolve شده است.
این همیشه یک Promise «زنجیرهای» جدید برمیگرداند.
بازگرداندن یک value از then باعث resolve شدن Promise «زنجیرهای» میشود.
پرتاب یک reason در then باعث reject شدن 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;
});
این پس از حدود ۱۰۰۰ میلیثانیه "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 خواهد بود.درک این نکته مهم است که به دلیل قواعد چرخهی حیات، وقتی یک Promise عمل reject را انجام میدهد، resolveی که حدود ۱۰۰۰ میلیثانیه بعد میآید بیصدا نادیده گرفته میشود، چون حالت داخلی پس از reject یا resolve شدن دیگر نمیتواند تغییر کند.
درک این نکته هم مهم است که بازگرداندن یک مقدار از یک Promise آن را resolve میکند و پرتاب یک مقدار آن را reject میکند.
وقتی promise1 resolve میشود و یک onResolved زنجیرهشده دارد: then(onResolved)، آن ادامه یک Promise جدید است که میتواند resolve یا reject شود.
وقتی promise1 reject میشود اما یک onRejected زنجیرهشده دارد: then(, onRejected)، آن ادامه یک Promise جدید است که میتواند resolve یا reject شود.
گاهی میخواهید خطاها را بگیرید و فقط زمانی ادامه دهید که Promise اصلی reject شود.
با promise.catch(onCatch)، تابع بازگشتی onCatch دلیلی را دریافت میکند که Promise اصلی با آن reject شده است.
این همیشه یک Promise «زنجیرهای» جدید برمیگرداند.
بازگرداندن یک value از catch باعث resolve شدن Promise «زنجیرهای» میشود.
پرتاب یک reason در catch باعث reject شدن 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);
در حدود ۱/۲ موارد، این پس از حدود ۱۰۰۰ میلیثانیه "Success!" را لاگ میکند.
در ۱/۲ دیگر موارد، این بلافاصله 42 را لاگ میکند.
promise1 resolve شود، catch نادیده گرفته میشود و به then میرسد و مقدار را لاگ میکند.
promise1 برابر resolved و "Success!" خواهد بود.promise2 برابر resolved و "done" خواهد بود؛promise1 reject شود، catch اجرا میشود، که مقداری را برمیگرداند و بنابراین زنجیره اکنون resolved است و به then میرسد و مقدار را لاگ میکند.
promise1 برابر rejected و "Nope!" خواهد بود.promise2 برابر resolved و "done" خواهد بود؛گاهی میخواهید پس از مشخص شدن سرانجام یک Promise، کدی را اجرا کنید، بیآنکه مهم باشد آن Promise resolve میشود یا reject.
با promise.finally(onSettled)، تابع بازگشتی onSettled هیچ مقداری دریافت نمیکند.
این همیشه یک Promise «زنجیرهای» جدید برمیگرداند.
بازگرداندن یک value از finally وضعیت و مقدار Promise اصلی را کپی میکند و value را نادیده میگیرد.
پرتاب یک reason در finally باعث reject شدن 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 resolve میشودfinalPromise با مقدار 'yay' resolve میشودSampled data: ...Promise completedrandomNumber برابر 4-5 است:
myPromise با دلیل 'Sampling did not result in a sample' reject میشودfinalPromise با دلیل Error('Sampling did not result in a sample') reject میشود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) ترجمهی text را از ذخیرهگاه API واکشی میکند و یک 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.
⚠ هشدار! ⚠
API جادویش را با تلهپورت کردن مترجمهای مختلف انجام میدهد، هر وقت که یک request برسد.
این کار بسیار پرهزینه است، بنابراین وقتی ترجمهای موجود است نباید فراخوانی شود.
متأسفانه همه دفترچهی راهنما را نمیخوانند، بنابراین سامانهای هست که افراد متخلف را بیرون میاندازد.
اگر api.request برای textی فراخوانی شود که موجود است، 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 reject میشود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 افزوده شود.
درخواست باید در صورت بروز خطا بهطور خودکار دوباره تلاش کند.
برای یک درخواست یکسان، نباید بیشتر از ۳ بار فراخوانی انجام دهد (مترجمهای فضایی را عصبانی نکنید!!!).
api.request خطایی برنگرداند، با undefined resolve کنید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 resolve شود، پیش از resolve کردن کیفیت را بررسی کنیدapi.fetch reject شود، بهجایش ترجمه را درخواست کنیدapi.request reject شود، خطا را منتقل کنید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?.
در Exercism ثبتنام کنید تا JavaScript را همراه با 37 مفهوم159 تمرین و مربیگری انسانی واقعی یاد بگیرید و در آن استاد شوید، همهی اینها رایگان.