11. برنامهنویسی رویدادمحور و توابع ناهمزمان

تابع ناهمزمان تابعی است که اجرای آن آغاز میشود اما نتیجهاش منتظر نمیماند. هنگامی که اجرا کامل میشود، تابع ناهمزمان یک رویداد را فعال میکند و نتیجهاش را از طریق آن رویداد منتقل مینماید.
این شیوه عملکرد برای اجرا در یک مرورگر وب بسیار مناسب است. در واقع، یک برنامه که در یک مرورگر اجرا میشود، یک برنامه مبتنی بر رویداد است: این برنامه به رویدادها واکنش نشان میدهد، که عمدتاً توسط کاربر (کلیکها، حرکت ماوس، ورودی متن و غیره) ایجاد میشوند. برنامههای جاوااسکریپت که در یک مرورگر اجرا میشوند، باید از طریق پروتکل HTTP با سرویسهای خارجی ارتباط برقرار کنند. فعالیتهای بومی HTTP در جاوا اسکریپت ناهمزمان هستند: آنها آغاز میشوند و دریافت پاسخ از سرویس خارجی مورد نظر، از طریق رویدادی که به مجموعه رویدادهای پردازششده توسط برنامه اضافه میشود، سیگنال داده میشود.
اسکریپتهای زیر توسط [node.js] اجرا میشوند و نه توسط مرورگر. [node.js] نیز در حالت رویدادمحور عمل میکند:
- [node.js]، مانند یک مرورگر، از یک حلقه رویداد برای اجرای یک اسکریپت استفاده میکند؛
- اجرای کد اصلی اسکریپت اولین رویدادی است که اجرا میشود؛
- اگر این کد اصلی وظایف ناهمزمان را آغاز کرده باشد، اجرای برنامه تا زمانی که این وظایف ناهمزمان به پایان برسند، ادامه مییابد. این وظایف هنگام اتمام، یک رویداد را فعال میکنند. این رویدادها در حلقه رویدادها صفبندی میشوند؛
- اسکریپت اصلی باید برای دریافت نتایج عملیاتهای ناهمزمان، به این رویدادها مشترک شود؛
- اسکریپت تنها زمانی کامل میشود که تمام رویدادهایی که ارسال کرده است پردازش شوند؛
11.1. اسکریپت [async-01]
اسکریپت زیر رفتار یک اسکریپت حاوی یک عمل غیرهمزمان را نشان میدهد.
''use strict';
// وارداتها
import moment from 'moment';
import { sprintf } from 'sprintf-js';
// شروع
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// setTimeout یک تایمر ۱۰۰۰ میلیثانیهای (پارامتر دوم) تنظیم میکند و بلافاصله شناسه تایمر را بازمیگرداند
// وقتی تایمر به مدت ۱۰۰۰ میلیثانیه سپری شد، یک رویداد صادر میکند که توسط زمانبندی اجرا صف میشود
// وقتی رویداد توسط محیط اجرا پردازش میشود، تابع (پارامتر اول) اجرا میشود
setTimeout(function () {
//این کد زمانی اجرا میشود که تایمر به 0 برسد
console.log("[fin de l'action asynchrone setTimeout],", heure(débutScript));
}, 1000)
// قبل از پیام تابع داخلی تایمر نمایش داده خواهد شد
console.log("[fin du code principal du script],", heure(débutScript));
//ابزار نمایش زمان و مدت
function heure(début) {
// زمان فعلی
const now = moment(Date.now());
//قالببندی زمان
let result = "heure=" + now.format("HH:mm:ss:SSS");
// آیا باید مدت زمان محاسبه شود؟
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
//قالببندی زمان و مدت
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// نتیجه
return result;
}
- خط ۴: کتابخانه [moment] برای امکانپذیر ساختن قالببندی تاریخ (خط ۲۷) وارد میشود؛
- خط ۵: کتابخانه [sprintf-js] برای امکانپذیر ساختن قالببندی دورهها (خط ۳۴) وارد شده است؛
- خط ۸: زمان شروع اسکریپت ثبت میشود؛
- خط ۹: این با استفاده از متد [heure] در خطوط ۲۰–۳۴ نمایش داده میشود؛
- خطوط ۱۴–۱۷: تابع [setTimeout] دارای دو پارامتر (f، duration) است: f تابعی است که زمانی اجرا میشود که [durée] میلیثانیه سپری شده باشد؛
- خط 14: وقتی اسکریپت اجرا میشود، تابع [setTimeout] فراخوانی میشود:
- خط ۱۷: یک تایمر ۱۰۰۰ میلیثانیهای راهاندازی میشود و شمارش معکوس تا رسیدن به ۰ آغاز میگردد. تابع [setTimeout] به محض راهاندازی تایمر و شروع شمارش معکوس خاتمه مییابد. این تابع منتظر پایان شمارش معکوس نمیماند. این تابع عددی را که نشاندهنده تایمر مورد استفاده است بازمیگرداند و اجرای برنامه به دستور بعدی، خط ۲۰، منتقل میشود. در اینجا، نتیجه تابع [setTimeout] استفاده نمیشود؛
- خط ۱۶: این پیام در پایان تأخیر ۱۰۰۰ میلیثانیهای در تابع [setTimeout] نمایش داده خواهد شد؛
- خطوط ۱۵–۱۶: تابع f، اولین پارامتر تابع [setTimeout]، در پایان تأخیر ۱۰۰۰ میلیثانیهای اجرا خواهد شد. سپس پیام موجود در خط ۱۶ نمایش داده میشود؛
- خط ۲۰: این پیام قبل از پیام خط ۱۶ نمایش داده خواهد شد؛
تابع زمان:
- خط ۲۳: این تابع یک پارامتر اختیاری، [heure]، را میپذیرد که زمان شروع عملیاتی است که باید مدت زمان آن نمایش داده شود؛
- خطوط ۲۵–۲۷: زمان فعلی محاسبه و قالببندی میشود؛
- خط ۲۹: اگر پارامتر [début] موجود باشد، باید مدت زمان محاسبه شود؛
- خط ۳۰: مدتزمان عملیات. این به صورت تعداد میلیثانیهها بیان میشود؛
- خطوط ۳۱–۳۲: این تعداد میلیثانیه به ثانیه و میلیثانیه تفکیک میشود؛
- خط ۳۴: مدتزمان به زمان اضافه میشود؛
اجرا
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-01.js"
[début du script], heure=09:26:40:238
[fin du code principal du script], heure=09:26:40:246, durée= 0 seconde(s) et 11 millisecondes
[fin de l'action asynchrone setTimeout], heure=09:26:41:249, durée= 1 seconde(s) et 14 millisecondes
[Done] exited with code=0 in 1.672 seconds
- خط ۴: میبینیم که عمل غیرهمزمان [setTimeOut] تقریباً ۱ ثانیه پس از پایان کد اصلی اسکریپت به پایان رسیده است؛
- خط ۶: زمان نمایش داده شده در خط ۳، زمانی است که کد اصلی به پایان رسیده است. اگر کد اصلی وظایف غیرهمزمان را آغاز کرده باشد، اسکریپت تا زمانی که تمام وظایف غیرهمزمان اجرا نشدهاند، کامل نمیشود. مدت زمان نمایش داده شده در خط ۶، کل زمان اجرای اسکریپت (کد اصلی + وظایف غیرهمزمان) است؛
تابع [setTimeout] به ما امکان میدهد تا وظایف غیرهمزمان را در محیطی مشابه [node.js] شبیهسازی کنیم. در واقع، تابع [setTimeout] مانند یک وظیفه غیرهمزمان رفتار میکند:
- این تابع بلافاصله یک نتیجه – در این مورد، یک شماره تایمر – را با استفاده از مکانیزم استاندارد بازگشت تابع بازمیگرداند؛
- این تابع ممکن است بعداً (که در مثال بالا هنوز اینطور نیست) نتایج دیگری را از طریق رویدادها بازگرداند، که سپس توسط حلقه رویدادهای [node.js] پردازش میشوند؛
- در اکثر موارد بعدی، دو رویداد از این دست وجود خواهد داشت:
- یک رویداد که میتوان آن را [success] نامید، که توسط وظیفهٔ ناهمزمان که وظیفهاش را با موفقیت به پایان رسانده است، منتشر میشود. یک مقدار داده—که نتیجهٔ وظیفه است—با رویداد منتشرشده مرتبط است؛
- یک رویداد که میتوان آن را [failure] نامید، که توسط وظیفه ناهمزمان که در تکمیل وظیفهاش ناموفق بوده، منتشر میشود. یک قطعه داده—معمولاً یک شیء که خطا را توصیف میکند—با رویداد منتشرشده مرتبط است. برای مثال، خطاهای احتمالی در یک وظیفه اینترنتی ناهمزمان میتوانند شامل «شبکه در دسترس نیست»، «سرور پیدا نشد»، «زمانبندی فراتر رفت» و غیره باشند.
- کد اصلی که یک وظیفه ناهمزمان را راهاندازی کرده است، میتواند برای رویدادهایی که این وظیفه احتمالاً منتشر میکند، مشترک شود. هنگامی که یکی از این رویدادها منتشر میشود، کد اصلی مطلع شده و میتواند اجرای یک تابع خاص را که برای رسیدگی به این رویداد طراحی شده است، فعال کند. این تابع، دادههایی را که وظیفه ناهمزمان با رویداد منتشر شده مرتبط کرده است، به عنوان پارامتر دریافت میکند؛
11.2. اسکریپت [async-02]
در این اسکریپت، تابع غیرهمزمان [setTimeout] رویدادهایی را برای ارسال دادهها به کدی که به آنها مشترک شده است، منتشر میکند.
دسترسی به رویدادهای [node.js] نیازمند کتابخانههای اضافی است. ما کتابخانه [events] را انتخاب کردهایم که آن را در کنار [npm] نصب میکنیم:

اسکریپت [async-02] به شرح زیر است:
''use strict';
// توابع غیرهمزمان میتوانند با ارسال یک رویداد نتیجه را بازگردانند
// کد اصلی میتواند با مشترک شدن در رویدادهای صادرشده این نتایج را بازیابی کند
// واردات
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';
// شروع
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// یک صادرکننده رویداد
const eventEmitter = new EventEmitter();
// setTimeout یک تایمر ۱۰۰۰ میلیثانیهای (پارامتر دوم) تنظیم میکند و بلافاصله شماره تایمر را بازمیگرداند
// وقتی تایمر به مدت ۱۰۰۰ میلیثانیه سپری شد، یک رویداد صادر میکند که توسط زمانبندی اجرا صف میشود
// وقتی رویداد توسط محیط اجرا پردازش میشود، تابع (پارامتر اول) اجرا میشود
setTimeout(function () {
//این کد زمانی اجرا میشود که تایمر به ۰ برسد
console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
//یک رویداد برای نشان دادن در دسترس بودن یک نتیجه فعال میشود
eventEmitter.emit("timer1Success", { success: 4 });
// رویداد دیگری برای نشان دادن در دسترس بودن نتیجهٔ دیگر ایجاد میشود
eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)
// اشتراکی برای رویداد [timer1Success] انجام میشود
eventEmitter.on('timer1Success', (result) => {
console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});
// مشترک رویداد [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});
// قبل از پیامهای رویداد ارسالشده توسط تابع مرتبط با [timer1] نمایش داده خواهد شد
console.log("[fin du code principal du script],", heure(débutScript));
//ابزار نمایش زمان و مدت
function heure(début) {
// زمان فعلی
const now = moment(Date.now());
// قالببندی زمان
let result = "heure=" + now.format("HH:mm:ss:SSS");
// آیا باید مدت زمان محاسبه شود؟
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
//قالببندی زمان و مدت
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// نتیجه
return result;
}
نظرات
- خط ۹: کلاس [EventEmitter] از کتابخانه [events] وارد میشود. این یک ویژگی جدید است: تا به حال، ما فقط اشیاء و توابع صریح را وارد کرده بودیم؛
- خط ۱۵: ما با استفاده از کلمه کلیدی [new]، یک کلاس [EventEmitter] را نمونهسازی کرده و یک صادرکننده رویداد [node.js] ایجاد میکنیم؛
- خطوط ۲۰–۲۷: تابع غیرهمزمان [setTimeout]. این تابع هنگام اجرا دو رویداد منتشر خواهد کرد:
- خط 24، رویداد [timer1Success] با مقدار مرتبط {success: 4};
- خط ۲۶، رویداد [timer1Failure] با مقدار مرتبط {failure: 6}؛
- یک تابع غیرهمزمان میتواند هر تعداد رویدادی را که بخواهد منتشر کند. قبلاً اشاره کردیم که این تابع معمولاً یکی از دو رویداد [success, failure] را منتشر میکند، نه هر دو را، همانطور که ما اینجا انجام میدهیم؛
- خط ۲۰: اجرای [setTimeout] آنی است: یک تایمر تنظیم میشود و شمارهی آن به کد فراخوانیکننده بازگردانده میشود. رویدادها بعداً منتشر خواهند شد، در این مورد ۱ ثانیه بعد؛
- ارسال رویدادها بیفایده است اگر کدی برای رسیدگی به آنها هنگام وقوع وجود نداشته باشد. به همین دلیل است که کد اصلی باید در صورت تمایل به رسیدگی به رویدادها، به هر دو رویداد [timer1Success, timer1Failure] مشترک شود، بهویژه برای بازیابی دادههای مرتبط با این رویدادها؛
- خطوط ۳۰–۳۲: کد اصلی به رویداد [timer1Success] مشترک میشود. هنگامی که منبع رویداد [node.js] این رویداد را پردازش میکند، این تابع را که پارامتر دوم متد [eventEmitter.on] است فراخوانی میکند و دادههای مرتبط با رویداد [timer1Success] (که در اینجا [result] نامیده میشود) را به آن پاس میدهد؛
- خط ۳۱: تابع رسیدگی به رویداد، jSON دادههای مرتبط با رویداد را به همراه زمان فعلی نمایش خواهد داد؛
- خطوط ۳۵–۳۷: با استفاده از کدی مشابه، کد اصلی به رویداد [timer1Failure] مشترک میشود؛
- مشترک شدن به یک رویداد (پارامتر اول) بلافاصله کد موجود در تابع [callback] (پارامتر دوم) را اجرا نمیکند. این کد تنها پس از وقوع رویداد اجرا خواهد شد؛
- خط ۴۰: کد اصلی اسکریپت به پایان رسیده است، اما خود اسکریپت نه، زیرا کد اصلی یک وظیفهٔ ناهمزمان را آغاز کرده است. کل اسکریپت تنها زمانی پایان مییابد که این وظیفهٔ ناهمزمان تکمیل شود؛
این موضوع با نتایج بهدستآمده نشان داده میشود:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-02.js"
[début du script], heure=09:34:58:909
[fin du code principal du script], heure=09:34:58:916, durée= 0 seconde(s) et 10 millisecondes
[setTimeout, fin du timer d'1 s], heure=09:34:59:929, durée= 1 seconde(s) et 23 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=09:34:59:931, durée= 1 seconde(s) et 25 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=09:34:59:932, durée= 1 seconde(s) et 26 millisecondes, via l'événement [timer1Failure]
[Done] exited with code=0 in 1.627 seconds
- خط ۳: پایان کد اصلی ۱۰ میلیثانیه پس از شروع اسکریپت؛
- خط ۴: شروع تابع محصور شده در تایمر ۱۰۰۰ میلیثانیهای، تقریباً ۱ ثانیه پس از شروع اسکریپت؛
- خط ۵: پردازش رویداد [‘timer1Success’]، ۲ میلیثانیه بعد؛
- خط ۶: پردازش رویداد [‘timer1Failure’]، ۱ میلیثانیه پس از رویداد [‘timer1Success’]؛
- خط ۸: پایان کل اسکریپت با مجموع مدت زمان ۱.۶۲۷ ثانیه؛
11.3. اسکریپت [async-03]
اسکریپت زیر جنبه دیگری از حلقه رویداد [node.js] را نشان میدهد:
- حلقه رویدادها را یکی پس از دیگری، عموماً به ترتیبی که میرسند، اجرا میکند. برخی از اسکریپتهای OS اولویتهایی را به رویدادها اختصاص میدهند که سپس به جای ترتیب ورود، بر اساس اولویت پردازش میشوند؛
- حلقه در هر لحظه تنها یک رویداد را اجرا میکند. رویداد بعدی تنها پس از تکمیل رویداد قبلی پردازش میشود. بنابراین در یک سیستم مبتنی بر رویداد باید از نوشتن کدی که پردازنده را برای مدت طولانی به خود اختصاص میدهد اجتناب کرد، زیرا رویدادها در زمان وقوع پردازش نمیشوند و تنها بعداً، هنگامی که حلقه رویداد به آنها میرسد، پردازش میشوند. نتیجه یک برنامه است که چندان «پاسخگو» نیست؛
اسکریپت [async-03] این پدیده را نشان میدهد:
''use strict';
// توابع ناهمزمان میتوانند با ارسال یک رویداد نتیجه بازگردانند
// کد اصلی میتواند با مشترک شدن در رویدادهای منتشرشده این نتایج را بازیابی کند
// واردات
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';
// شروع
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// یک صادرکننده رویداد
const eventEmitter = new EventEmitter();
// setTimeout یک تایمر ۱۰۰۰ میلیثانیهای (پارامتر دوم) تنظیم میکند و بلافاصله شماره تایمر را بازمیگرداند
// وقتی تایمر به مدت ۱۰۰۰ میلیثانیه سپری شد، یک رویداد صادر میکند که توسط زمانبندی اجرا صف میشود
//وقتی رویداد توسط محیط اجرا پردازش شود، تابع (پارامتر اول) اجرا میشود
setTimeout(function () {
//این کد زمانی اجرا میشود که تایمر به ۰ برسد
console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
//یک رویداد برای نشان دادن در دسترس بودن یک نتیجه فعال میشود
eventEmitter.emit("timer1Success", { success: 4 });
// رویداد دیگری برای نشان دادن در دسترس بودن نتیجهٔ دیگر فعال میشود
eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)
// مشترک رویداد [timer1Success]
eventEmitter.on('timer1Success', (result) => {
console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});
// مشترک رویداد [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});
// یک کد همگام نسبتاً پرمصرف منابع که مانع از تکمیل کد اصلی قبل از پایان [timer1] شد
for (let i = 0; i < 1000000; i++) {
for (let j = 0; j < 10000; j++) {
i + i ^ 2 + i ^ 3;
}
}
// قبل از پیامهای رویدادی که توسط تابع مرتبط با [timer1] صادر میشوند، نمایش داده خواهد شد
console.log("[fin du script],", heure(débutScript));
//ابزار نمایش زمان و مدت
function heure(début) {
...
}
توضیحات
- این کد مشابه مثال قبلی، [async-02]، است که خطوط 39 تا 44 به آن اضافه شده است؛
- خطوط ۲۰–۲۷: تابع [setTimeout] برنامهریزی شده است تا پس از تأخیری به مدت یک ثانیه، یک تابع غیرهمزمان داخلی را اجرا کند. پس از گذشت این یک ثانیه، اجرای تابع غیرهمزمان تایمر بلافاصله انجام نمیشود: یک رویداد در حلقه اجرایی قرار میگیرد تا آن را درخواست کند. اگر حلقه اجرایی در حال پردازش رویداد دیگری باشد، اجرای تابع غیرهمزمان تایمر باید منتظر بماند؛
- خطوط ۲۰–۲۷: به محض اینکه تابع [setTimeout] تایمر خود را برای تأخیر یک ثانیهای تنظیم کرد، پردازنده را آزاد میکند و کنترل را به کد فراخواننده بازمیگرداند. کد فراخواننده با خطوط ۳۰–۳۷ ادامه مییابد، که شامل اشتراکگذاری رویدادها هستند و زمان اجرای ناچیزی دارند؛
- کد اصلی با خطوط ۴۰–۴۴ ادامه مییابد که یک حلقه با ۱,۰۱۰ تکرار را تشکیل میدهند. این کد زمانی اجرا میشود که تایمر رویداد «پایان تأخیر ۱ ثانیهای» خود را فعال کند. این رویداد سپس در حلقه رویدادها قرار میگیرد اما باید منتظر بماند تا کد اصلی اسکریپت به اجرای خود پایان دهد تا پردازش شود؛
- خط ۴۷: پایان کد اصلی اسکریپت. تنها پس از این نمایش نهایی است که رویداد پایان تایمر میتواند پردازش شود و تابع غیرهمزمان داخلی در [setTimeout] میتواند اجرا شود؛
اسکریپت نتایج زیر را تولید میکند:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-03.js"
[début du script], heure=08:55:02:665
[fin du code principal du script], heure=08:55:11:789, durée= 9 seconde(s) et 131 millisecondes
[setTimeout, fin du timer d'1 s], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Failure]
[Done] exited with code=0 in 9.796 seconds
توضیحات
- خط ۳: میبینیم که کد اصلی اسکریپت ۹ ثانیه طول کشید تا اجرا شود. هر رویدادی که ممکن است در این مدت رخ داده باشد، در حلقه رویداد صفبندی شدند؛
- خط ۴: میبینیم که رویداد [fin du timer]، ۵ میلیثانیه پس از پایان کد اصلی پردازش شد. این رویداد تقریباً ۱ ثانیه پس از شروع اسکریپت صادر شده بود، اما مجبور بود ۸ ثانیه دیگر صبر کند تا در نهایت پردازش شود؛
نکته کلیدی این مثال این است که در یک سیستم رویدادمحور، کد هرگز نباید برای مدت طولانی پردازنده را اشغال کند. اگر کد همگام دارید که زمان اجرای طولانی دارد، باید «راهی پیدا کنید» تا آن را به وظایف کوتاهتر غیرهمگام تقسیم کنید که تکمیل خود را از طریق یک رویداد اعلام میکنند.
11.4. اسکریپت [async-04]
اسکریپت [async-04] مکانیزم دیگری به نام [Promise]، یعنی یک «وعده نتیجه» (result promise)، را نشان میدهد. این مکانیزم نیاز به رسیدگی صریح به رویدادهای [node.js] را از بین میبرد. این کار به طور ضمنی انجام میشود و بنابراین توسعهدهنده میتواند وجود این رویدادها را نادیده بگیرد. با این حال، درک آنها به توسعهدهنده این امکان را میدهد که بهتر متوجه نحوه کار [Promise] شود، که در نگاه اول پیچیده است.
نوع [Promise] یک کلاس جاوااسکریپت است. سازنده (constructor) آن یک تابع ناهمزمان را به عنوان پارامتر میپذیرد که دو پارامتر به نامهای سنتی [resolve] و [reject] را به آن پاس میدهد. این پارامترها میتوانند نامهای متفاوتی داشته باشند؛
- سازنده کلاس [Promise] دو کار انجام میدهد:
- یک رویداد ایجاد میکند تا اجرای تابع [function(resolve, reject)] را که بهعنوان پارامتر به آن ارسال شده است، آغاز کند، اما منتظر نتیجه آن نمیماند و بلافاصله یک شیء [Promise] را به کد فراخوانیکننده بازمیگرداند. این شیء میتواند چهار وضعیت داشته باشد:
- [pending]: عملیات غیرهمزمان که [Promise] را آغاز کرد هنوز تکمیل نشده است؛
- [fulfilled]: عمل غیرهمزمان که [Promise] را بازگردانده است با موفقیت به پایان رسید؛
- [rejected]: عملیات غیرهمزمان که مقدار بازگشتی [Promise] را برگرداند، با خطا مواجه شد؛
- [settled]: عملیاتی که [Promise] را بازگردانده است، تکمیل شده است؛
- یک رویداد ایجاد میکند تا اجرای تابع [function(resolve, reject)] را که بهعنوان پارامتر به آن ارسال شده است، آغاز کند، اما منتظر نتیجه آن نمیماند و بلافاصله یک شیء [Promise] را به کد فراخوانیکننده بازمیگرداند. این شیء میتواند چهار وضعیت داشته باشد:
هنگامی که سازنده نتیجه خود را بازمیگرداند، شیء [Promise] ایجاد شده در وضعیت [pending] قرار دارد و در انتظار نتایج تابع ناهمزمان است؛
- (ادامه)
- وظیفهٔ ناهمزمان در خطوط ۲–۵ بلافاصله اجرا میشود. وظایف ناهمزمان اغلب وظایف ورودی/خروجی هستند که به شرح زیر تقسیم میشوند:
- اجرای کد همگام برای آغاز عملیات ورودی/خروجی با یک مؤلفه دیگر، برای مثال یک سرور راه دور؛
- انتظار برای پاسخ از آن مؤلفه؛
- پردازش این پاسخ؛
- وظیفهٔ ناهمزمان در خطوط ۲–۵ بلافاصله اجرا میشود. وظایف ناهمزمان اغلب وظایف ورودی/خروجی هستند که به شرح زیر تقسیم میشوند:
فاز دوم – انتظار برای مؤلفهٔ خارجی – است که بیشترین منابع را مصرف میکند. به جای انتظار:
- دریافت دادههای درخواستشده از مؤلفهٔ خارجی با یک رویداد سیگنالدهی خواهد شد؛
- در کد همگام که پس از فاز ۱ میآید (خط ۷ کد نمونه)، ما به این رویداد مشترک میشویم و سپس، در مقطعی، به حلقه رویداد [node.js] بازمیگردیم. رویداد بعدی در لیست رویدادهای در انتظار سپس پردازش خواهد شد؛
- در فاز ۲، اجرا بهطور موازی اما روی دستگاههای مختلف انجام میشود:
- پردازنده برای حلقه رویداد؛
- یک دستگاه خارجی (دیسک، پایگاه داده، سرور راه دور) برای بازیابی دادههای درخواستی؛
- در پایان فاز ۲، هنگامی که عملیات ورودی/خروجی دادههای درخواستی را بازیابی کرد، رویدادی برای نشان دادن در دسترس بودن نتیجه ورودی/خروجی ایجاد میشود. این رویداد سپس به سایر رویدادها در صف رویدادها میپیوندد؛
- وقتی نوبتش برسد، پردازش خواهد شد. تابع مرتبط با این رویداد (خط ۷ کد نمونه) سپس اجرا خواهد شد؛
این حالت عملیاتی به جلوگیری از زمان بیکاری کمک میکند: زمانی که پردازنده منتظر پاسخی از یک دستگاه جانبی است که از خود کندتر است؛
- (ادامه)
- پس از آنکه وظیفه ناهمزمان در خطوط ۲ و ۵ راهاندازی شده و کار خود را به پایان رساند، میتواند با استفاده از دو تابع [resolve, reject] که سازنده [Promise] به عنوان پارامتر به آن پاس داده است، نتیجهای را به کد فراخواننده بازگرداند. کنوانسیون به این صورت است:
- وظیفهٔ ناهمزمان از طریق [resolve(result)] موفقیت را اعلام میکند. این به منزله افزودن یک رویداد به حلقه رویداد [node.js] است که میتواند با نام [resolved] فراخوانده شود و [result] دادههای مرتبط آن است؛
- وظیفه ناهمزمان از طریق [reject(error)] خطا را گزارش میکند. این به معنای قرار دادن یک رویداد در حلقه رویداد [node.js] است که میتواند [rejected] نامیده شود و دادههای مرتبط آن [error] است—که معمولاً یک شیء حاوی جزئیات خطایی است که رخ داده است؛
- بنابراین، کد فراخوانی باید برای هر دو این رویدادها مشترک شود تا زمانی که نتیجه تابع ناهمزمان در دسترس قرار گرفت، مطلع شود؛
- پس از آنکه وظیفه ناهمزمان در خطوط ۲ و ۵ راهاندازی شده و کار خود را به پایان رساند، میتواند با استفاده از دو تابع [resolve, reject] که سازنده [Promise] به عنوان پارامتر به آن پاس داده است، نتیجهای را به کد فراخواننده بازگرداند. کنوانسیون به این صورت است:
به محض اینکه اجرای وظیفه ناهمزمان که در [Promise] محصور شده است، تکمیل شد، وضعیت شیء [promise] که توسط سازنده [Promise(…)] بازگردانده شده است، تغییر میکند:
- رویداد [resolved] آن را از وضعیت [pending] به [resolved] تغییر میدهد؛
- رویداد [rejected] وضعیت خود را از [pending] به [rejected] تغییر میدهد؛
مشترک شدن به رویدادهای [resolved] و [rejected] از وظیفه ناهمزمان با استفاده از متدهای کلاس [Promise] با سینتکس زیر انجام میشود:
promise.then(f1).catch(f2).finally(f3);
که در آن:
- f1 تابعی است که زمانی اجرا میشود که وضعیت [promise] از [pending] به [resolved] تغییر کند، یعنی زمانی که وظیفهٔ ناهمزمان با موفقیت به پایان رسیده باشد. این تابع مقدار [result] را بهعنوان پارامتر دریافت میکند که توسط دستور [resolve(result)] از وظیفه ناهمزمان ارسال شده است؛
- f2 تابعی است که زمانی اجرا میشود که وضعیت [promise] از [pending] به [rejected] تغییر کند، یعنی زمانی که وظیفهٔ ناهمزمان در انجام کار خود ناموفق بوده است. این تابع مقدار [error] را بهعنوان پارامتر دریافت میکند که توسط دستور [reject(errror)] از وظیفه ناهمزمان ارسال شده است؛
- f3 تابعی است که پس از اجرای متدهای [then] یا [catch] اجرا میشود و بنابراین همیشه اجرا میشود. این تابع هیچ پارامتری دریافت نمیکند؛
این نحو، رویدادهایی را که ما برای آنها اشتراکگذاری میکنیم، کاملاً پنهان میسازد. با این حال، این یک اشتراکگذاری است و مانند مثال قبلی، فوراً توابع [f1, f2, f3] را اجرا نمیکند. این توابع زمانی اجرا خواهند شد—یا نخواهند شد—که یکی از رویدادهای [resolved, rejected] که برای آن اشتراکگذاری کردهایم رخ دهد.
اسکریپت [async-04] این مکانیزم را نشان میدهد:
'use strict';
// امکان بازیابی نتایج (موفقیت، شکست) یک تابع غیرهمزمان وجود دارد
//بدون استفاده صریح از رویدادها، به لطف کلاس [Promise]
//این کلاس بهطور ضمنی از رویدادها استفاده میکند، اما این رویدادها در کد قابل مشاهده نیستند
// واردات
import moment from 'moment';
import { sprintf } from 'sprintf-js';
// start
const débutScript = moment(Date.now());
console.log("[début du script],", heure(débutScript));
//تعریف یک وظیفهٔ غیرهمزمان با استفاده از یک وعده [Promise]
// وظیفهٔ ناهمزمان پارامتر سازنده است [Promise]
const débutPromise1 = moment(Date.now());
const promise1 = new Promise(function (resolve) {
// ثبت
console.log("[début fonction asynchrone de promise1],", heure(débutPromise1));
// کد ناهمزمان
setTimeout(function () {
console.log("[fin fonction asynchrone de promise1],", heure(débutPromise1));
// وظیفهٔ ناهمزمان با استفاده از تابع [resolve] نتیجه را بازمیگرداند
// وعده سپس عملی میشود
resolve('[réussite]');
}, 1000)
});
//نتیجهٔ وعده را میتوان با استفاده از [promise1] بازیابی کرد
// پس از اینکه حل یا رد شده باشد
//دستور زیر یک اشتراکگذاری برای رویداد [resolved] از طریق متد [then] است
//و به رویداد [rejected] از طریق متد [catch]
// روش [finally] اجرا میشود چه بعد از 'then' باشد چه بعد از 'catch'
promise1.then(result => {
// اگر وعده موفق شود [evt resolved]
console.log(sprintf("[promise1.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
//حالت خطا [evt rejected]
console.log(sprintf("[promise1.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
// در همه موارد اجرا شد
console.log("[promise1.finally]", heure(débutPromise1));
});
//تعریف یک وظیفه ناهمزمان با استفاده از یک وعده [Promise]
const débutPromise2 = moment(Date.now());
const promise2 = new Promise(function (resolve, reject) {
// لاگ
console.log("[début fonction asynchrone de promise2],", heure(débutPromise1));
// وظیفهٔ ناهمزمان
setTimeout(function () {
console.log("[fin fonction asynchrone de promise2],", heure(débutPromise2));
// وظیفهٔ غیرهمزمان با استفاده از تابع [reject] نتیجه را بازمیگرداند
// سپس وعده رد میشود
reject('[échec]');
}, 2000)
});
// نتیجهٔ وعده را میتوان با استفاده از [promise2] تعیین کرد
// پس از اینکه حل یا رد شده باشد
promise2.then(result => {
// اگر وعده موفقیتآمیز باشد [evt resolved]
console.log(sprintf("[promise2.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
//در صورت بروز خطا [evt rejected]
console.log(sprintf("[promise2.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
//در همه موارد اجرا شد
console.log(sprintf("[promise2.finally], %s", heure(débutPromise2)));
});
// قبل از پیامهای توابع ناهمزمان و پیامهای رویدادهای مرتبط نمایش داده خواهد شد
console.log("[fin du code principal du script],", heure(débutScript));
//ابزار
function heure(début) {
// زمان فعلی
const now = moment(Date.now());
//قالببندی زمان
let result = "heure=" + now.format("HH:mm:ss:SSS");
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
//قالببندی مدت زمان
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// نتیجه
return result;
}
نظرات
- خطوط ۱۸–۲۸: ایجاد یک [Promise promise1]. تابع غیرهمزمان آن پس از یک ثانیه نتیجه خود را از طریق یک رویداد بازمیگرداند. پس از آغاز این عملیات غیرهمزمان (با راهاندازی یک تایمر)، ما منتظر بازگشت نتیجه آن نمیمانیم، بلکه بلافاصله به کد در خط ۳۵ میرویم؛
- خطوط ۳۵–۴۴: ما به دو رویداد [resolved, rejected] که تابع غیرهمزمان داخلی در [promise1] ممکن است صادر کند، مشترک میشویم؛
- خطوط ۴۶–۷۱: همان توالی کد قبلی برای یک promise دوم، [promise2]، تکرار میشود؛
- خط ۷۴: بخش اصلی اسکریپت به پایان رسیده است، اما خود اسکریپت به طور کامل نه، زیرا دو عمل غیرهمزمان آغاز شدهاند. ما به حلقه رویداد بازمیگردیم، جایی که در مقطعی یکی از رویدادهای [resolved, rejected] از وعدههای [promise1, promise2] رخ خواهد داد. سپس آن پردازش خواهد شد؛
- سپس کنترل به حلقه رویداد بازمیگردد. در آن نقطه، رویداد دوم [resolved, rejected] از وعدههای [promise1, promise2] هنگام وقوع پردازش خواهد شد؛
اجرا
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-04.js"
[début du script], heure=09:39:05:950, durée= 0 seconde(s) et 3 millisecondes
[début fonction asynchrone de promise1], heure=09:39:05:958, durée= 0 seconde(s) et 0 millisecondes
[début fonction asynchrone de promise2], heure=09:39:05:959, durée= 0 seconde(s) et 1 millisecondes
[fin du code principal du script], heure=09:39:05:960, durée= 0 seconde(s) et 13 millisecondes
[fin fonction asynchrone de promise1], heure=09:39:06:977, durée= 1 seconde(s) et 19 millisecondes
[promise1.then], heure=09:39:06:980, durée= 1 seconde(s) et 21 millisecondes, result=[réussite]
[promise1.finally] heure=09:39:06:982, durée= 1 seconde(s) et 24 millisecondes
[fin fonction asynchrone de promise2], heure=09:39:07:976, durée= 2 seconde(s) et 17 millisecondes
[promise2.catch], heure=09:39:07:978, durée= 2 seconde(s) et 19 millisecondes, result=[échec]
[promise2.finally], heure=09:39:07:980, durée= 2 seconde(s) et 21 millisecondes
[Done] exited with code=0 in 2.589 seconds
نظرات
- خط ۳: تابع غیرهمزمان [promise1] اجرا میشود، اما منتظر اتمام آن نمیمانیم، که با یک رویداد سیگنال داده خواهد شد؛
- خط ۴: تابع غیرهمزمان [promise2] اجرا میشود، اما منتظر اتمام آن نمیمانیم، که با یک رویداد سیگنال داده خواهد شد؛
- خط ۵: پایان کد اصلی و بازگشت به حلقه رویداد؛
- خط ۶: پردازش رویداد [fin fonction asynchrone de promise1]. وضعیت [promise1] به [resolved] تغییر خواهد کرد. یک رویداد این تغییر را سیگنال میدهد؛
- خط ۷: از آنجایی که [promise2] هنوز کار خود را به پایان نرسانده است، رویداد [promise1 resolved] که به تازگی به حلقه اضافه شده است، توسط متد [promise1.then] و سپس توسط متد [promise.finally] (خط ۸) پردازش خواهد شد؛
- خطوط ۹–۱۱: همین سازوکار زمانی رخ میدهد که [promise2] از حالت [pending] به [resolved] منتقل میشود؛
11.5. اسکریپت [async-05]
بیایید به کد سازنده برای یک شیء [Promise] بازگردیم:
در خط ۲، وظیفهٔ غیرهمزمان برای [Promise] راهاندازی میشود. این وظیفه اغلب به پارامترهای بیشتری نیاز دارد تا صرفاً پارامترهای [resolve, reject] که توسط تابع محصورکننده به آن ارسال شده است. در این مورد، ایجاد [Promise] در داخل یک تابع محصور شده است که پارامترهای مورد نیاز تابع ناهمزمان آن را به آن ارسال میکند:
اسکریپت زیر:
- دو تابع ناهمزمان را تعریف میکند که یک [Promise] را بازمیگردانند؛
- آنها را بهطور موازی اجرا میکند و پیش از انجام یک کار مشخص، منتظر پایان هر دو میماند؛
'use strict';
// میتوان توابع غیرهمزمان را تعریف کرد که نوعی مانند [Promise] را بازگردانند
// سپس میتوان آنها را با کلمه کلیدی [async] برچسبگذاری کرد
// واردات
import moment from 'moment';
import { sprintf } from 'sprintf-js';
// شروع
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// یک تابع ناهمزمان که یک وعده را بازمیگرداند [Promise]
function async01(p1) {
return new Promise((resolve) => {
console.log("[début de la tâche asynchrone async01]");
// وظیفهٔ ناهمزمان
const débutAsync01 = moment(Date.now());
setTimeout(function () {
console.log("[fin de la tâche asynchrone async01],", heure(débutAsync01));
// وظیفهٔ غیرهمزمان ممکن است نتیجهای پیچیده بازگرداند
resolve({
prop1: [10, 20, 30],
prop2: "abcd",
prop3: p1,
});
}, 1000)
});
}
// یک تابع غیرهمزمان که یک promise را بازمیگرداند [Promise]
function async02(p1, p2) {
return new Promise(resolve => {
console.log("[début de la tâche asynchrone async02]");
// وظیفهٔ ناهمزمان
const débutAsync02 = moment(Date.now());
setTimeout(function () {
console.log("[fin de la tâche asynchrone async02],", heure(débutAsync02));
// وظیفهٔ ناهمزمان ممکن است نتیجهای پیچیده بازگرداند
resolve({
prop1: [11, 21, 31],
prop2: "xyzt",
prop3: p1 + p2
});
}, 2000)
})
}
// هر دو تابع غیرهمزمان بهطور موازی اجرا میشوند
// و منتظر میمانیم تا هر دو به پایان برسند
//بلوک «then» تنها زمانی اجرا میشود که هر دو تابع رویداد [resolved] را فراخوانی کرده باشند.
// بلوک `catch` به محض اینکه هر یک از دو تابع رویداد را ایجاد کند اجرا میشود [rejected]
Promise.all([async01(10), async02(10, 20)])
//نتیجه یک آرایه است [result1, result2] جایی که [result1] نتیجه منتشر شده توسط یک [resolve] از [async01]
//و [result2] نتیجهای است که توسط یک [resolve] از [async02] تولید میشود
.then(result => {
console.log(sprintf("[promise-all success], %s, result=%j", heure(débutScript), result));
})
//نتیجهای است که توسط اولین [reject] از یکی از دو تابع غیرهمزمان بازگردانده میشود
.catch(error => {
console.log(sprintf("[promise-all error], %s, erreur=%j", heure(débutScript), error));
})
// 'finally' پس از 'then' یا 'catch' اجرا میشود
.finally(() => {
console.log(sprintf("[promise-all finally], %s", heure(débutScript)));
});
// قبل از پیامهای حاصل از توابع غیرهمزمان و رویدادهای مرتبط نمایش داده خواهد شد
console.log("[fin du code principal du script],", heure(débutScript));
//ابزار
function heure(début) {
// زمان فعلی
const now = moment(Date.now());
//قالببندی زمان
let result = "heure=" + now.format("HH:mm:ss:SSS");
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
//قالببندی مدت زمان
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// نتیجه
return result;
}
توضیحات
- خطوط ۱۵–۳۰: تابع [async01] تعریف شده است که نتیجه خود را پس از ۱ ثانیه از طریق یک رویداد تایمر بازمیگرداند. تابع [async01] در نتیجه آن در خط ۲۶ استفاده میشود؛
- خطوط ۳۳–۴۷: ما همین کار را با تابع [async02] انجام میدهیم، که نتیجه خود را پس از ۲ ثانیه از طریق یک رویداد تایمر بازمیگرداند. تابع [async02] دو پارامتر میگیرد که در نتیجه آن در خط ۴۴ استفاده میشوند؛
- وقتی دو تابع [async01, async02] فراخوانی میشوند:
- راهاندازی خواهد شد؛
- دو وعده (promise) از نوع [promise1, promise2] به کد فراخوانی بازگردانده خواهد شد؛
- اجرا سپس به کد فراخوانی بازمیگردد و به اجرای خود ادامه میدهد؛
- پس از تقریباً ۱ ثانیه، [async01] یک رویداد صادر خواهد کرد تا نشان دهد که وظیفهاش را به پایان رسانده است. رویداد مذکور به همراه رویداد، در حلقه رویداد (event loop) مرتبط با نتیجهای که توسط [async01] ارسال شده است، صفگذاری خواهد شد؛
- پس از تقریباً ۲ ثانیه، همین فرآیند برای [async02] رخ خواهد داد؛
- خط ۵۴: اکنون است که توابع غیرهمزمان [async01, async02] (با نشانههای async01(10) و async02(10,20)) اجرا میشوند. آنها در داخل آرایهای که به عنوان پارامتر به متد [Promise.all] ارسال شده است، اجرا میشوند. میدانیم که [async01, async02] هر دو یک promise را به کد فراخوانیکننده بازمیگردانند. بنابراین، پارامتر برای [Promise.all] آرایهای از دو promise است؛
- [Promise.all([promise1, promise2, …, promisen]).then(f1).catch(f2).finally(f3)] یک اشتراک رویداد است:
- [Promise.all] از نوع [Promise] است؛
- تابع [f1] از متد [then] زمانی اجرا خواهد شد که تمام وعدههای [promise1, promise2, …, promisen] در آرایه پارامتر متد [all] ازاز حالت [pending] به حالت [resolved]. به عبارت دیگر، [f1] زمانی اجرا خواهد شد که همه وعدهها در آرایه با موفقیت تکمیل شده باشند؛
- تابع [f2] از متد [catch] به محض اینکه هر یک از وعدهها در آرایه وضعیت خود را از [pending] به [rejected] تغییر دهد، اجرا خواهد شد. به عبارت دیگر، [f2] به محض اینکه یکی از وعدهها در آرایه با شکست مواجه شود، اجرا میشود؛
- تابع [f3] از متد [finally] پس از اجرای یکی از متدهای [then, catch] اجرا خواهد شد، و بنابراین همیشه اجرا میشود؛
اجرای کد نتایج زیر را تولید میکند:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-05.js"
[début du script], heure=12:17:17:367
[début de la tâche asynchrone async01]
[début de la tâche asynchrone async02]
[fin du code principal du script], heure=12:17:17:375, durée= 0 seconde(s) et 10 millisecondes
[fin de la tâche asynchrone async01], heure=12:17:18:391, durée= 1 seconde(s) et 17 millisecondes
[fin de la tâche asynchrone async02], heure=12:17:19:389, durée= 2 seconde(s) et 14 millisecondes
[promise-all success], heure=12:17:19:390, durée= 2 seconde(s) et 25 millisecondes, result=[{"prop1":[10,20,30],"prop2":"abcd","prop3":10},{"prop1":[11,21,31],"prop2":"xyzt","prop3":30}]
[promise-all finally], heure=12:17:19:392, durée= 2 seconde(s) et 27 millisecondes
[Done] exited with code=0 in 2.572 seconds
- خطوط ۶–۷: دو وظیفه غیرهمزمان [async01, async02] راهاندازی میشوند. آنها به صورت موازی اجرا میشوند. این اجرای کد آنها نیست که به صورت موازی انجام میشود، بلکه انتظارهای متناظر آنها برای دادههای درخواستی به طور همزمان رخ میدهند؛
- خط ۵: کد اسکریپت اصلی به پایان رسیده است. تنها کاری که باقی مانده، انتظار برای تکمیل دو وظیفه غیرهمزمان [async01, async02] است؛
- خط ۶: وظیفه غیرهمزمان [async01] تقریباً ۱ ثانیه پس از راهاندازی کامل میشود. این تابع از طریق [resolve] نتیجه را بازمیگرداند، بنابراین promise آن در آرایه در خط 56 کد از [pending] به [resolved] تغییر وضعیت میدهد. این برای فراخوانی متد [then]، خطوط ۵۹–۶۰ کد، کافی نیست؛
- خط ۷: وظیفه غیرهمزمان [async02] تقریباً ۲ ثانیه پس از راهاندازی کامل میشود. این تابع با استفاده از [resolve] نتیجه را بازمیگرداند، بنابراین promise آن در آرایه در خط ۵۶ کد از [pending] به [resolved] تغییر وضعیت میدهد. متد [then] به محض اینکه حلقه رویداد اجازه دهد، اجرا خواهد شد؛
- خط ۸: متد [then] از [Promise.all] اجرا میشود. این متد به عنوان پارامتر یک آرایه [result1, result2] دریافت میکند، که در آن [result1] نتیجه بازگردانده شده توسط [async01] است، و [result2] نتیجه بازگردانده شده توسط [async02] است؛
- خط ۹: متد [finally] از [Promise.all] اجرا میشود؛
11.6. اسکریپت [async-06]
این اسکریپت جدید نشان میدهد که چگونه استفاده ترکیبی از کلیدواژههای [async / await] امکان میدهد کد ناهمزمان شبیه کد همزمان به نظر برسد. مدیریت رویدادها بهطور کامل پنهان شده و درک کد را آسانتر میکند.
ما به مثال قبلی بازمیگردیم و تغییرات زیر را اعمال میکنیم:
- ما یک تابع غیرهمزمان سوم [async03] اضافه میکنیم که نتیجه خود را با استفاده از متد [Promise.reject] برمیگرداند و بدین ترتیب به حلقه رویداد سیگنال میدهد که در تکمیل وظیفهاش 'ناموفق' بوده است؛
- ما سه تابع غیرهمزمان [async01, async02, async03] را بهصورت متوالی اجرا میکنیم. در مثال قبلی، ما توابع غیرهمزمان [async01, async02] را بهصورت موازی اجرا کرده بودیم؛
- پیش از معرفی کلمات کلیدی [async/await]، اجرای متوالی عملیات غیرهمزمان با استفاده از توابع درونی [Promise] انجام میشد. به محض اینکه چندین اقدام ناهمزمان برای اجرا به این روش وجود داشت، تعداد وعدهها (promises) به همان نسبت افزایش مییافت و کد کمتر خوانا میشد؛
- با کلمات کلیدی [async/await]، اجرای متوالی وظایف غیرهمزمان با استفاده از نحوی مشابه نحوی اجرای وظایف همزمان انجام میشود:
// تابع ناهمزمان – با استفاده از async/await
async function main() {
// اجرای متوالی وظایف ناهمزمان
try {
// اجرا در حین انتظار برای [async01]
const result1 = await async01(...);
console.log("[async01 result]=", result1);
// اجرا با انتظار برای [async02]
const result2 = await async02(...);
console.log("[async02 result]=", result2);
// اجرا در حین انتظار برای [async03]
const result3 = await async03(...);
console.log("[async03 result]=", result3);
} catch (error) {
//یکی از عملیاتهای غیرهمزمان با شکست مواجه شد
console.log(sprintf("[sequential error]= %j, %s", error));
} finally {
// تکمیل شد
console.log("[fin exécution séquentielle des tâches asynchrones],");
}
- خط ۶: تابع غیرهمزمان [async01] فراخوانی میشود (با استفاده از کلمه کلیدی `await`) و ما منتظر میمانیم تا نتیجه خود را از طریق یکی از متدهای [Promise.resolve, Promise.reject] منتشر کند. بنابراین این یک عملیات مسدودکننده است؛
- خط ۶: کلمه کلیدی [await] عملیات ناهمزمان [async01] را به یک عملیات مسدودکننده تبدیل میکند. میدانیم که عملیات [async01] نتیجهای را به دو روش بازمیگرداند:
- این یک شیء [Promise] را تقریباً بلافاصله به کد فراخوانیکننده بازمیگرداند؛
- سپس نتیجه را از طریق متدهای [Promise.resolve, Promise.reject] در حلقه رویدادها منتشر میکند. این نتیجهٔ دوم است که توسط [result1] در خط ۶ بازیابی میشود. رسیدگی مبتنی بر رویداد به اقدام [async01] نامرئی شده است؛
- اگر نتیجه [result] از [async01] توسط [Promise.resolve(result)] منتشر شود، این نتیجه در خط ۶ به [result1] اختصاص داده میشود و اجرای برنامه در خط ۷ ادامه مییابد؛
- اگر نتیجه [async01] توسط [Promise.reject] منتشر شود، این یک استثنا را ایجاد میکند و اجرای کد به خط ۱۴، جایی که بلاک catch قرار دارد، پرش میکند. پارامتر بند [catch]، شیء خطا است که توسط [async01] با عبارت [Promise.reject(error)] منتشر میشود. وظیفهٔ غیرهمزمان همچنین میتواند خطا را از طریق [throw(error)] منتشر کند. شیء [error] همان شیئی است که در [catch(error)] بازیابی میشود؛
- کلمه کلیدی [await] باید در داخل تابعی که با کلمه کلیدی [async] در خط ۲ پیشفرض شده باشد، ظاهر شود. این کلمه کلیدی نشان میدهد که تابع [main] یک تابع ناهمزمان است؛
- در عبارت [await f(…)]، [f] باید یک تابع غیرهمزمان باشد که یک شیء [Promise] را به کد فراخوانیکننده بازمیگرداند؛
- ما همین کار را برای عمل غیرهمزمان [async02] در خط ۹ و [async03] در خط ۱۲ انجام میدهیم؛
با استفاده از کلیدواژههای [async / await]، میتوان وظایف ناهمزمان را بهصورت موازی با استفاده از نحو زیر اجرا کرد:
try {
// اجرای موازی وظایف ناهمزمان
const result = await Promise.all([async01(...), async02(...), async03(...)]);
console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
} catch (error) {
//یکی از عملیات ناهمزمان با شکست مواجه شد
console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
} finally {
// تکمیل شد
console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
}
- خط ۳: این یک عملیات مسدودکننده است: ما منتظر میمانیم تا سه وظیفه غیرهمزمان در آرایه [async01(..), async02(..), async03(..)] نتایج خود را با استفاده از یکی از متدهای [Promise.resolve, Promise.reject] به حلقه رویداد منتشر کنند؛
- اگر سه وظیفهٔ غیرهمزمان نتایج خود را با استفاده از [Promise.resolve] منتشر کنند، آنگاه مقدار ثابت [result] آرایهٔ [result1, result2, result3] خواهد بود، که در آن:
- [result1] نتیجهای است که توسط [async01] با استفاده از عبارت [Promise.resolve(result1)] منتشر میشود؛
- [result2] نتیجه منتشر شده توسط [async02] با استفاده از عبارت [Promise.resolve(result2)] است؛
- [result3] نتیجهای است که توسط [async03] با استفاده از عبارت [Promise.resolve(result3)] منتشر میشود؛
- اگر هر یک از سه وظیفه نتیجه خود را با عبارت [Promise.reject(error)] منتشر کند، آنگاه یک استثنا رخ میدهد؛
- ثابت [result] در خط ۳ مقداردهی نشده است؛
- اجرا مستقیماً به [catch] در خط 5 ادامه مییابد؛
- پارامتر (error) بلوک catch، شیء (error) منتشر شده توسط عبارت [Promise.reject(error)] است؛
با ترکیب این دو نحو، میتوان وظایف ناهمزمان را به صورت سری یا موازی اجرا کرد، و همگی از نحو مشابه کد همزمان استفاده میکنند. بنابراین این نحو بسیار خواناتر از موارد قبلی است و باید به آن اولویت داده شود. این نحو [async / await] تنها از نسخه ۶ ECMAScript در دسترس بوده است. هنوز مقدار زیادی کد جاوااسکریپت وجود دارد که از وعدههای [Promise] استفاده میکند. به همین دلیل، درک نحوه کار آنها نیز مهم است.
کد کامل اسکریپت برای [async-06] به شرح زیر است:
''use strict';
// اجرای موازی یا متوالی چندین وظیفه ناهمزمان
// با استفاده از کلمات کلیدی async / await
// وارداتها
import moment from 'moment';
import { sprintf } from 'sprintf-js';
// شروع
const débutScript = moment(Date.now());
console.log("[début du code principal du script],", heure());
// یک تابع ناهمزمان که یک [Promise] را بازمیگرداند
function async01(débutAsync01) {
return new Promise(function (resolve) {
console.log("[début fonction asynchrone async01],", heure());
// تابع ناهمزمان
setTimeout(function () {
console.log("[fin fonction asynchrone async01],", heure(débutAsync01));
// عملیات غیرهمزمان ممکن است نتیجهای پیچیده بازگرداند
// موفقیتآمیز در اینجا
resolve({
prop1: [11, 21, 31],
prop2: "abcd"
});
}, 1000)
});
}
// یک تابع ناهمزمان که یک [Promise] را بازمیگرداند
function async02(débutAsync02) {
console.log("[début fonction asynchrone async02],", heure());
return new Promise(function (resolve) {
// تابع ناهمزمان
setTimeout(function () {
console.log("[fin fonction asynchrone async02],", heure(débutAsync02));
// یک عمل ناهمزمان میتواند نتیجهٔ پیچیدهای را بازگرداند
// موفقیت در اینجا
resolve({
prop1: [12, 22, 32],
prop2: "xyzt"
});
}, 2000)
})
}
// یک تابع غیرهمزمان که یک [Promise] را بازمیگرداند
function async03(débutAsync03) {
console.log("[début fonction asynchrone async03],", heure());
return new Promise((resolve, reject) => {
// تابع ناهمزمان
setTimeout(function () {
console.log("[fin fonction asynchrone async03],", heure(débutAsync03));
// یک عمل ناهمزمان میتواند نتیجهای پیچیده بازگرداند
// در اینجا خطا
reject({
prop1: [13, 23, 33],
prop2: "échec"
});
}, 3000)
})
}
// تابع ناهمزمان – با استفاده از async/await
async function main() {
const débutSequential = moment(Date.now());
// اجرای متوالی وظایف ناهمزمان
console.log("------------ exécution séquentielle des tâches asynchrones lancée ------------------------")
try {
// اجرا در حین انتظار برای [async01]
const débutAsync01 = moment(Date.now());
const result1 = await async01(débutAsync01);
console.log("[async01 result]=", result1);
// اجرا در حین انتظار برای [async02]
const débutAsync02 = moment(Date.now());
console.log("début async02-------------", heure());
const result2 = await async02(débutAsync02);
console.log("[async02 result]=", result2);
// اجرا در انتظار تکمیل [async03]
const débutAsync03 = moment(Date.now());
console.log("début async03-------------", heure());
const result3 = await async03(débutAsync03);
console.log("[async03 result]=", result3);
} catch (error) {
//یکی از عملیاتهای غیرهمزمان با شکست مواجه شد
console.log(sprintf("[sequential error]= %j, %s", error, heure(débutSequential)));
} finally {
// تکمیل شد
console.log("[fin exécution séquentielle des tâches asynchrones],", heure(débutSequential));
}
const débutParallel = moment(Date.now());
// اجرای موازی وظایف ناهمزمان
console.log("------------ exécution parallèle des tâches asynchrones lancée ------------------------")
try {
const result = await Promise.all([async01(débutParallel), async02(débutParallel), async03(débutParallel)]);
console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
} catch (error) {
//یکی از عملیات ناهمزمان با شکست مواجه شده است
console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
} finally {
// تکمیل شد
console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
}
// تکمیل شد
console.log("[fin de la fonction main],", heure(débutSequential));
}
// اجرای تابع غیرهمزمان «main»
main();
// قبل از نمایش پیامهای مختلف از توابع ناهمزمان و رویدادهای آنها نمایش داده خواهد شد
console.log("[fin du code principal du script],", heure(débutScript));
//ابزار
function heure(début) {
//زمان فعلی
const now = moment(Date.now());
//قالببندی زمان
let result = "heure=" + now.format("HH:mm:ss:SSS");
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
//قالببندی مدت زمان
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// نتیجه
return result;
}
نتایج اجرای کد به شرح زیر است:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-06.js"
[début du code principal du script], heure=15:02:00:152
------------ exécution séquentielle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:00:161
[fin du code principal du script], heure=15:02:00:164, durée= 0 seconde(s) et 15 millisecondes
[fin fonction asynchrone async01], heure=15:02:01:165, durée= 1 seconde(s) et 4 millisecondes
[async01 result]= { prop1: [ 11, 21, 31 ], prop2: 'abcd' }
début async02------------- heure=15:02:01:253
[début fonction asynchrone async02], heure=15:02:01:254
[fin fonction asynchrone async02], heure=15:02:03:265, durée= 2 seconde(s) et 12 millisecondes
[async02 result]= { prop1: [ 12, 22, 32 ], prop2: 'xyzt' }
début async03------------- heure=15:02:03:268
[début fonction asynchrone async03], heure=15:02:03:268
[fin fonction asynchrone async03], heure=15:02:06:285, durée= 3 seconde(s) et 18 millisecondes
[sequential error]= {"prop1":[13,23,33],"prop2":"échec"}, heure=15:02:06:289, durée= 6 seconde(s) et 129 millisecondes
[fin exécution séquentielle des tâches asynchrones], heure=15:02:06:291, durée= 6 seconde(s) et 131 millisecondes
------------ exécution parallèle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:06:292
[début fonction asynchrone async02], heure=15:02:06:293
[début fonction asynchrone async03], heure=15:02:06:294
[fin fonction asynchrone async01], heure=15:02:07:294, durée= 1 seconde(s) et 2 millisecondes
[fin fonction asynchrone async02], heure=15:02:08:298, durée= 2 seconde(s) et 6 millisecondes
[fin fonction asynchrone async03], heure=15:02:09:297, durée= 3 seconde(s) et 5 millisecondes
[parallel error], heure=15:02:09:298, durée= 3 seconde(s) et 6 millisecondes, erreur={"prop1":[13,23,33],"prop2":"échec"}
[fin exécution parallèle des tâches asynchrones],heure=15:02:09:299, durée= 3 seconde(s) et 7 millisecondes
[fin de la fonction main], heure=15:02:09:300, durée= 9 seconde(s) et 140 millisecondes
[Done] exited with code=0 in 9.668 seconds