برنامه نویسی

وعده: نحوه استفاده و چند نکته!

صحبت نکردن در مورد Promise در جاوا اسکریپت یک حذف آشکار است. در واقع، مقالات زیادی در مورد Promise نوشته شده است که می توانید آنها را از طریق گوگل پیدا کنید یا گهگاه در انجمن های مرتبط با برنامه نویسی با آنها برخورد کنید. اما از آنجایی که Promise یک مفهوم مهم است و هر کسی روش متفاوتی برای توضیح آن دارد، من همچنان تصمیم گرفتم این مقاله را بنویسم.

وقتی برای اولین بار جاوا اسکریپت را یاد گرفتم، Promise گیج کننده ترین چیز بود. فکر می‌کردم آن را می‌فهمم و می‌دانم چگونه از آن استفاده کنم، اما در واقعیت، هنوز بسیاری از دست و پا زدن‌های طولانی و دردناک وجود داشت که درس‌های ارزشمندی به من آموخت. من مقالات زیادی در مورد Promise به دو زبان انگلیسی و ویتنامی خواندم و به تدریج همه چیز معنی پیدا کرد و به من کمک کرد تا آن را به درستی درک کنم و از آن استفاده کنم.

این مقاله به مفاهیم Promise نمی پردازد، بلکه به برخی نکات و سوء تفاهمات احتمالی می پردازد. هدف آن ارائه یک درک کلی به خوانندگان و کمک به آنها برای جلوگیری از برخی اشتباهات هنگام نوشتن کد است.

وعده چیست؟

Promise یک شی است که نشان دهنده نتیجه آینده است. به عبارت دیگر، یک Promise نتیجه یک تابع ناهمزمان را نشان می دهد.

یک Promise دارای سه حالت مربوط به سه نتیجه ممکن یک تابع ناهمزمان است:

  • pending حالت اولیه است، منتظر نتیجه است.
  • fulfilled حالت موفق با مقدار نتیجه است.
  • rejected حالت شکست، با مقدار خطای اختیاری است.

وعده

به عنوان مثال، بیایید یک Promise ایجاد کنیم که یک عدد را می گیرد x و برمی گرداند fulfilled اگر x بر 2 بخش پذیر است و rejected در غیر این صورت بیان کنید

function isEven(x) {
  return new Promise((resolve, reject) => {
    if (x % 2 === 0) {
      resolve(true);
    } else {
      reject(new Error('x is not even'));
    }
  });
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اصل، یک وعده نشان دهنده یک نتیجه آینده است. در مثال بالا، ایجاد یک Promise ضروری نیست زیرا تمام اقدامات داخل آن انجام می شود isEven تابع همزمان هستند بنابراین، چه زمانی باید از Promise استفاده کنیم و چه چیزی یک تابع را ناهمزمان می کند؟

چه چیزی یک تابع را ناهمزمان می کند؟ من در بسیاری از مقالات به آن اشاره کرده ام. برخی از وظایف I/O ارائه شده توسط توابع ناهمزمان نمی توانند فوراً نتیجه را برگردانند. آنها به عوامل خارجی مانند سرعت سخت افزار، سرعت شبکه و غیره بستگی دارند. انتظار برای این اقدامات می تواند زمان زیادی را هدر دهد یا باعث تنگناهای جدی شود.

به عنوان مثال، هنگام درخواست GET به آدرس https://example.com، پردازش دیگر به سرعت CPU بستگی ندارد. به سرعت شبکه شما نیز بستگی دارد. هر چه شبکه سریعتر باشد، نتیجه را سریعتر دریافت خواهید کرد. در جاوا اسکریپت، ما آن را داریم fetch تابع ارسال درخواست است و ناهمزمان است و یک Promise را برمی گرداند.

fetch('https://example.com');
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای رسیدگی به نتیجه یک Promise، از آن استفاده می کنیم then و catch به ترتیب برای موارد موفقیت و شکست.

fetch('https://example.com')
    .then(res => console.log(res))
    .catch(err => console.log(err));
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اگر پس از مدتی پردازش، fetch است fulfilled، تابع داخل then فعال خواهد شد. در غیر این صورت، اگر fetch است rejected، تابع داخل catch بلافاصله اعدام خواهد شد.

گاهی اوقات ممکن است با سوالاتی مانند پیش بینی نتیجه مثال زیر مواجه شوید:

console.log('1');

fetch('https://example.com')
    .then(res => console.log(res))
    .catch(err => console.log(err))
    .finally(() => console.log('3'));

console.log('2');
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این سوال در واقع برای آزمایش درک شما از رفتار ناهمزمان در جاوا اسکریپت است. نتیجه به جای 1، 3، 2، 1، 2، 3 خواهد بود. این به دلیل ماهیت پردازش ناهمزمان است. از آنجایی که نتیجه رفتار ناهمزمان در آینده برگردانده خواهد شد، جاوا اسکریپت فکر می‌کند، “بسیار خوب، این تابع ناهمزمان هنوز نتیجه فوری ندارد. بگذارید همینطور باشد و به پردازش دستورات زیر ادامه دهید. وقتی همه چیز انجام شد، بررسی کنید که آیا یک تابع دارد یا خیر. نتیجه.” برای اطلاعات بیشتر در مورد نحوه عملکرد پردازش ناهمزمان، می توانید به مقالات برنامه نویسی ناهمزمان در جاوا اسکریپت مراجعه کنید.

Promise چند روش استاتیک مفید برای موارد استفاده مختلف دارد، مانند all، allSettled، any، و race.

Promise.all آرایه ای از Promises را می گیرد، یک Promise برمی گرداند و در آن قرار می گیرد fulfilled زمانی که همه Promises در آرایه موفقیت آمیز هستند را بیان کنید. در غیر این صورت، آن را در rejected حالت زمانی که حداقل یک Promise در آرایه با شکست مواجه شود.

Promise.allSettled شبیه است به Promise.all اما همیشه نتایج تمام Promises در آرایه را بدون در نظر گرفتن موفقیت یا شکست برمی گرداند. هر دو Promise.all و Promise.allSettled زمانی که نیاز دارید چندین تابع ناهمزمان را فوراً بدون اهمیت دادن به ترتیب نتایج اجرا کنید مفید هستند.

از سوی دیگر، Promise.race بدون در نظر گرفتن موفقیت یا شکست، نتیجه Promise را که ابتدا تسویه شده است، برمی گرداند. Promise.any اولین وعده انجام شده را در آرایه برمی گرداند. race و any زمانی مناسب هستند که چندین وعده دارید که اقدامات مشابهی را انجام می دهند و نیاز به بازگشت بین نتایج دارند.

Promise جایگزین “Callback Hell” می شود

جای تعجب است که بدانید جاوا اسکریپت قبلاً Promise نداشت. بله، درست شنیدی. این واقعیت باعث شد که Node.js نیز در روزهای اولیه Promise نداشته باشد، و این بزرگترین پشیمانی است که سازنده Node.js دارد.

پیش از این، تمام وظایف ناهمزمان از طریق تماس پاسخ داده می شد. ما یک تابع callback تعریف می کنیم تا نتیجه را در آینده مدیریت کند.

به عنوان مثال، یک تابع درخواست ناهمزمان اولیه بود XMLHttpRequest. نتیجه را از طریق تماس پاسخ داد.

function reqListener() {
  console.log(this.responseText);
}

const req = new XMLHttpRequest();
req.addEventListener('load', reqListener);
req.open('GET', 'https://example.com');
req.send();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

reqListener تابعی است که زمانی فراخوانی می شود که نتیجه ای از درخواست به وجود داشته باشد https://example.com. مدیریت رفتار ناهمزمان با تماس‌های برگشتی مشکلاتی را به همراه داشت، از جمله آنچه که معمولاً به عنوان “جهنم پاسخ به تماس” شناخته می‌شود.

به عنوان مثال، یک تابع ناهمزمان fnA یک تابع تماس با دو پارامتر دریافت می کند: data نشان دهنده نتیجه موفقیت آمیز و err نشان دهنده خطا توابع مشابه شامل fnB، fnCو غیره اگر بخواهیم این توابع پردازشی را با هم ترکیب کنیم، باید آنها را در داخل یکدیگر لانه کنیم.

fnA((data1, err1) => {
    fnB((data2, err2) => {
        fnC((data3, err3) => {
        ....  
        });
    });
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هنگامی که تعداد بیش از حد تماس‌های برگشتی به این شکل نوشته می‌شود، کد ما به معنای واقعی کلمه به “جهنم پاسخ به تماس” تبدیل می‌شود. آشفته می شود و مانع از روند خواندن و درک می شود.

Promise برای ارائه یک رویکرد جدید برای مدیریت رفتار ناهمزمان معرفی شد. ما هنوز از تماس‌های برگشتی استفاده می‌کنیم، اما می‌توانیم «جهنم» را با اتصال متوالی همه چیز محدود کنیم then.

fnA()
    .then(data => fnB(data))
    .then(data => fnC(data))
    ...  
    .catch(err => console.log(err));
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پس از آن، بسیاری از توابع مبتنی بر تماس ناهمزمان با استفاده از آن بازنویسی شدند new Promise. در Node.js، ما حتی یک ماژول داخلی به نام util.promisify مخصوص این تبدیل داریم. تماس‌های برگشتی همچنان پشتیبانی می‌شوند، اما با مزایایی که Promise به ارمغان می‌آورد، بسیاری از کتابخانه‌های جدید از Promise به عنوان پیش‌فرض برای مدیریت رفتار ناهمزمان استفاده می‌کنند.

Promise in Loops

هنگام استفاده از Promise بدون درک کامل ماهیت آن، اشتباهات تاسف بار زیادی وجود دارد و یکی از آنها مدیریت عملیات ناهمزمان متوالی در یک حلقه است.

فرض کنید باید بین 5 صفحه تکرار کنید، تماس های API صفحه بندی شده را از 1 تا 5 انجام دهید و مقادیر بازگشتی را به ترتیب در یک آرایه به هم متصل کنید.

const results = [];
for (let i = 1; i <= 5; i++) {
  fetch(`https://example.com?page=${i}`)
    .then(response => response.json())
    .then(data => results.push(data));
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کد بالا یک حلقه برای واکشی داده ها از صفحه 1 تا صفحه 5 ایجاد می کند و داده های برگشتی به داخل results آرایه. در نگاه اول، نتیجه آرایه ای از داده ها به ترتیب از صفحه اول تا صفحه آخر خواهد بود. با این حال، در واقعیت، هر بار که اجرا می شود، متوجه خواهید شد که داده ها در results به صورت تصادفی سفارش داده شده است

یاد آوردن fetch، یک Promise را برمی گرداند که نشان دهنده یک نتیجه آینده است… در حالی که for سعی می‌کند در سریع‌ترین زمان ممکن از آن عبور کند، می‌توانید آن را به عنوان همه 5 مورد در نظر بگیرید fetch فراخوانی و شروع “تقریبا فورا” دستورات. در این مرحله، سرعت CPU اهمیت کمتری دارد – این سرعت شبکه است که تعیین می کند کدام یک fetch دستور اولین نتیجه را دارد. به محض رسیدن به نتیجه، بلافاصله آن را به داخل هل می دهد results، در نتیجه داده ها به صورت تصادفی اضافه می شوند.

برای حل این مشکل راه های مختلفی وجود دارد. مثلا:

const results = [];
fetch('https://example.com?page=1')
  .then(response => response.json())
  .then(data => {
    results.push(data);
    return fetch('https://example.com?page=2');
  })
  .then(response => response.json())
  .then(data => {
    results.push(data);
    return fetch('https://example.com?page=3');
  })
  ...  
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این احمقانه است و هیچکس اینگونه کد نمی نویسد. تصور کنید اگر 1000 صفحه وجود داشته باشد چه می شود؟ این یک شوخی است، اما مثال بالا سعی می کند این ایده را نشان دهد که چگونه باید قبل از ادامه درخواست بعدی منتظر تکمیل درخواست قبلی بود.

Bluebird یک کتابخانه Promise بسیار خوب است که بسیاری از توابع کاربردی را برای سهولت کار با توابع ناهمزمان ارائه می دهد.

مثال بالا را می توان با استفاده از بازنویسی کرد each عملکرد ارائه شده توسط Bluebird.

const results = [];
Promise.each([1, 2, 3, 4, 5], (i) => {
  return fetch(`https://example.com?page=${i}`)
    .then(response => response.json())
    .then(data => results.push(data));
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Async/Await – The Missing Piece of Promise؟

برای ایجاد یک Promise از سینتکس استفاده می کنیم new Promise، اما با async/wait، ما به سادگی یک را اعلام می کنیم async تابع.

async function isEven(x) {
  if (x % 2 === 0) {
    return true;
  } else {
    throw new Error('x is not even');
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در حالی که Promise استفاده می کند then برای رسیدگی به نتیجه بازگشت در نقطه ای در آینده، async/wait به سادگی استفاده است await.

const result = await fetch('https://example.com');
const resultJSON = await result.json();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با شروع از Node.js نسخه 14.8، ما ویژگی انتظار سطح بالا را داریم، به این معنی await تماس ها دیگر محدود به داخل یک نمی باشد async تابع؛ آنها را می توان در خارج نیز استفاده کرد. قبل از آن، await فقط می تواند در داخل یک استفاده شود async تابع.

async function getData() {
    const result = await fetch('https://example.com');
    const resultJSON = await result.json();
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در حالی که Promise استفاده می کند .catch برای رسیدگی به خطاها، استفاده از همگام سازی/انتظار try...catch.

async function getData() {
    try {
        const result = await fetch('https://example.com');
        const resultJSON = await result.json();
    } catch (err) {
        console.log(err);
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بدیهی است که async/wait به ما امکان می‌دهد تا کدهای ناهمزمان را به‌صورت همزمان، بدون تماس و بدون نیاز به نوشتن بنویسیم. then. ما به سادگی منتظر نتیجه با استفاده هستیم await.

با این حال، این می تواند منجر به برخی سوء تفاهمات خطرناک مانند فراموشی شود await کلمه کلیدی برای منتظر نتیجه یک رفتار ناهمزمان یا به اشتباه فکر کردن که یک تابع ناهمزمان یک تابع همزمان است.

async function getData() {
    try {
        const result = await fetch('https://example.com');
        const resultJSON = await result.json();
        return resultJSON;
    } catch (err) {
        console.log(err);
    }
}

function main() {
    const data = getData();
    console.log(data);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اصل، getData یک Promise را برمی گرداند، بنابراین اگر قصد دریافت داده از یک تماس API باشد، برنامه فوق به درستی کار نمی کند. برای رفع این مشکل، باید آن را تغییر دهیم.

async function main() {
    const data = await getData();
    console.log(data);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بازگشت در مقابل بازگشت در انتظار

گاهی اوقات ممکن است با کدی مواجه شوید که به شکل زیر است:

async function fn() {
    ...  
    return await asyncFn();
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کارکرد fn در حال بازگشت است await از تابع ناهمزمان asyncFn. نویسنده احتمالا برای fn تا همیشه نتیجه را برگردانیم asyncFn، مانند await منتظر نتیجه عملکرد ناهمزمان است که به طور موثر می چرخد fn به یک تابع همزمان، یا به عبارت دیگر، “عدم” برگرداندن یک Promise.

متأسفانه این یک سوء تفاهم خطرناک است. در اصل، await فقط برای استفاده در سطح بالا یا داخل یک async عملکرد، و اگر آن است async، باید یک Promise را برگرداند. از این رو، fn همیشه یک وعده را برمی گرداند. پس چرا استفاده کنید return await asyncFn()?

به سادگی استفاده کنید return asyncFn() می تواند شما را کمی از فشار دادن کلید صرفه جویی کند، زیرا تفاوت قابل توجهی در مقایسه با آن وجود ندارد return await asyncFn(). تغییر بزرگ زمانی اتفاق می افتد که وجود داشته باشد try...catch که در return.

بیایید دو تابع زیر را در نظر بگیریم:

async function rejectionWithReturnAwait () {
  try {
    return await Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}

async function rejectionWithReturn () {
  try {
    return Promise.reject(new Error())
  } catch (e) {
    return 'Saved!'
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اولین تابع، rejectionWithReturnAwait، به عمد استفاده می کند return awaitو هنگامی که این وعده رد شد، catch بلوک به سرعت خطا را دریافت کرده و اجرا می کند return 'Saved!' بیانیه. این بدان معناست که تابع یک Promise حاوی رشته “ذخیره شده” را برمی گرداند.

متقابلا، rejectionWithReturn، بدون await، سعی در رد الف دارد Promise.reject(new Error())، و catch بلوک هرگز اجرا نمی شود.

منابع:

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا