Promise ها و علت پیروزی Async / Await

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 17 مرداد 1397
دسته بندی ها : جاوا اسکریپت

توابع ناهمگام هم یک چیز خوب، و هم یک چیز بد در JavaScript هستند. جنبه خوب آن‌ها این است که توابع ناهمگام بدون انسداد هستند، و بدین صورت بسیار سریع هستند؛ مخصوصا در Node.js. جنبه بد آن‌ها نیز این است که کار کردن با توابع ناهمگام می‌تواند سخت باشد؛ زیرا گاهی ممکن است مجبور شوید تا قبل از این که به مرحله بعدی بروید، منتظر بمانید تا یک تابع کامل شود و بتوانید callback آن را به دست بیاورید.

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

Promiseها علیه Callbackها

یک توسعه دهنده JavaScript یا Node.js، به خوبی درک می‌کند که تفاوت میان promiseها و Callbackها، و این که چگونه با هم کار می‌کنند، حیاتی است.

در میان این دو، تفاوت‌های کوچک اما مهمی وجود دارند. در هسته هر promise، یک callback وجود دارد که نوعی داده (یا خطا) را تا زمانی که به promise برسد، مدیریت کند.

مدیریت Callback

function done(err) {
    if (err) {
        console.log(err);
        return;
    }

    console.log('Passwords match!');
}

فراخوانی تابع validatePasssword():

function validatePassword(password) {
    if (password !== 'bambi') {
        return done('Password mismatch!');
    }

    return done(null);
}

قطعه کد زیر، یک بررسی کامل برای اعتبارسنجی یک رمز عبور را نشان می‌دهد:

// پس از فراهم شدن یک رشته (رمز عبور)
function validatePassword(password) {

    return new Promise((resolve, reject) => {
        // اعتبارسنجی رمز عبور
        if (password !== 'bambi') {
            // رمز عبور تطابق ندارد، یک خطا با وضعیت «رد شده» برگردان
            return reject('Invalid Password!');
        }

        // پسوورد تطابق دارد، وضعیت «به اتمام رسیده» را برگردان
        resolve();
    });
}

function done(err) {
    // اگر خطایی یافت شده است، پیغام آن را نشان بده
    if (err) {
        console.log(err);
        return; // stop execution
    }

    // یک وضعیت معتبر را نشان بده
    console.log('Password is valid!');
}

const password = 'foo';

// تابع اعتبارسنجی رمز عبور را فراخوانی کن
validatePassword(password)
    .then(() => {
        // اگر موفقت آمیز بود:
        done(null);
    })
    .catch(err => {
        // اگر خطایی بروز داد، پیغام آن را نشان بده:
        done(err);
    });

این کد به خوبی کامنت شده است، گرچه اگر گیج شده‌اید، بخش catch فقط زمانی اجرا می‌شود که تابع reject() از promise فراخوانی شده باشد. از آنجایی که رمز عبور تطابق ندارد، ما تابع reject() را فراخوانی می‌کنیم. از این رو، خطا را دریافت (catch) می‌کنیم و به تابع done() می‌فرستیم.

Promiseها

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

Promiseها سه state (وضعیت) خاص را فراهم می‌کنند:

  1. در حال پردازش - خروجی promise مورد نظر هنوز تعیین نشده است؛ زیرا عملیات ناهمگامی که نتیجه‌اش را خواهد ساخت، هنوز تکمیل نشده است.
  2. به اتمام رسیده - عملیات ناهمگام به پایان رسیده است و promise مورد نظر یک مقدار را در خود دارد.
  3. رد شده - عملیات ناهمگام با شکست مواجه شده است، و promise مورد نظر کامل نخواهد شد. در وضعیت «رد شده»، هر promise علت شکست عملیات را نیز نشان می‌دهد.

وقتی که یک promise در حال پردازش است، می‌تواند به وضعیت «به اتمام رسیده» یا «رد شده» منتقل شود. گرچه وقتی که یک promise به اتمام رسیده یا رد شده است، به هیچ وضعیت دیگری منتقل نمی‌شود و علت شکست آن تغییر نخواهد کرد.

جنبه منفی

یکی از مواردی که promiseها حل نمی‌کنند، چیزی به نام «Callback Hell» است، که در واقع مجموعه‌ای از فراخوانی توابع تو در تو است. درست است که برای یک فراخوانی مشکلی پیش نمی‌آید، اما برای چندین فراخوانی، اگر خواندن کد شما غیر ممکن نشود، حداقل کمی سخت می‌شود.

حلقه‌سازی در promiseها

برای جلوگیری از فراخوانی‌های تو در تو با JavaScript، باید فرض کنید که می‌توانید به سادگی به صورت حلقه‌وار از یک promise بگذرید، نتیجه را به یک آبجکت یا آرایه برگردانید، و وقتی که کار تمام شد، این promise متوقف شود. متاسفانه، این کار آنچنان هم آسان نیست. بر اساس طبیعت ناهمگام JavaScript، هیچ رویدادی به نام «done» وجود ندارد تا وقتی که کد شما تمام شد، فراخوانده شود.

روش مناسب برای مقابله با این وضعیت، استفاده از Promise.all() است. این تابع قبل از این که به عنوان «به اتمام رسیده» نشانه‌گذاری شود، منتظر می‌ماند تا تمام کارها به اتمام برسند.

مدیریت خطا

مدیریت خطا با چندین فراخوانی promise تو در تو مانند رانندگی با چشم بند است. به هیچ وجه نمی‌توانید بفهمید که کدام promise باعث بروز خطا شد. بهترین شانس شما، حذف کلی متد catch() است. به این صورت:

مرورگر

window.addEventListener('unhandledrejection', event => {
    // جلوگیری از خروجی خطا در کنسول
    event.preventDefault();

    // ارسال خطا به لاگ سرور
    log('Reason: ' + event.reason);
});

Node.js

process.on('unhandledRejection', (reason) => {

    console.log('Reason: ' + reason);

});

نکته: دو گزینه بالا، تنها راه‌های اطمینان از دریافت خطاها است. اگر متد catch() را فراموش کنید، توسط کد از بین می‌رود.

Async / await

Async / Await شما را قادر می‌سازد تا JavaScript ناهمگامی بنویسید که همگام به نظر می‌آید. در بخش‌های قبلی این پست، با promiseها آشنا شدید که قرار بود جریان ناهمگام را ساده‌تر کنند و از Callback Hell جلوگیری کنند، اما نتوانستند این کار را انجام دهند.

Callback Hell

Callback Hell اصطلاحی است که برای توضیح سناریو زیر استفاده می‌شود.

نکته: به عنوان یک مثال، در اینجا یک API می‌بینید که ۴ کاربر خاص را از یک آرایه دریافت می‌کند:

// کاربرانی که باید دریافت شوند
const users = [
    'W8lbAokuirfdlTJpnsNC5kryuHtu1G53',
    'ZinqxnohbXMQdtF6avtlUkxLLknRxCTh',
    'ynQePb3RB2JSx4iziGYMM5eXgkwnufS5',
    'EtT2haq2sNoWnNjmeyZnfUmZn9Ihfi8w'
];

// آرایه‌ای که پاسخ را در خود نگه می‌دارد
let response = [];

// ۴ کاربر را دریافت کن و پاسخ را به آرایه پاسخ ارسال کن
function getUsers(userId) {
    axios
        .get(`/users/userId=${users[0]}`)
        .then(res => {
            // پاسخ مربوط به کاربر ۱ را ذخیره کن
            response.push(res);

            axios
                .get(`/users/userId=${users[1]}`)
                .then(res => {
                    // پاسخ مربوط به کاربر ۲ را ذخیره کن
                    response.push(res);

                    axios
                        .get(`/users/userId=${users[2]}`)
                        .then(res => {
                            // پاسخ مربوط به کاربر ۳ را ذخیره کن
                            response.push(2);

                            axios
                                .get(`/users/userId=${users[3]}`)
                                .then(res => {
                                    // پاسخ مربوط به کاربر ۴ را ذخیره کن
                                    response.push(res);
                                })
                                .catch(err => {
                                    // مدیریت خطا
                                    console.log(err);
                                });
                        })
                        .catch(err => {
                            // مدیریت خطا
                            console.log(err);
                        });
                })
                .catch(err => {
                    // مدیریت خطا
                    console.log(err);
                });
        })
        .catch(err => {
            // مدیریت خطا
            console.log(err);
        });
}

خب، این کد بسیار نامناسب است و مقدار زیادی کدنویسی نیاز دارد. Async / Await جدیدتری و بهترین چیزی است که به JavaScript آمده است و ما را قادر می‌سازد تا نه تنها از Callback Hell جلوگیری کنیم، بلکه تضمین کنیم که کد ما مرتب است و خطاها به درستی دریافت می‌شوند. نکته شگفت‌انگیز درباره Async / Await این است که بر پایه Promiseها ساخته شده است، اما همچنان کد شما خوانا است؛ به گونه‌ای که همگام به نظر می‌رسد. قدرت Async / Await، در همینجاست.

نکته: در اینجا همان مثال API را می‌بینید که ۴ کاربر را از یک آرایه دریافت می‌کند، اما تقریبا حجمی معادل نیمی از کد قبلی دارد:

// کاربرانی که باید دریافت شوند
const users = [
    'W8lbAokuirfdlTJpnsNC5kryuHtu1G53',
    'ZinqxnohbXMQdtF6avtlUkxLLknRxCTh',
    'ynQePb3RB2JSx4iziGYMM5eXgkwnufS5',
    'EtT2haq2sNoWnNjmeyZnfUmZn9Ihfi8w'
];

// آرایه‌ای که پاسخ را در خود نگه می‌دارد
let response = [];

async function getUsers(users) {
    try {
        response[0] = await axios.get(`/users/userId=${users[0]}`);
        response[1] = await axios.get(`/users/userId=${users[1]}`);
        response[2] = await axios.get(`/users/userId=${users[2]}`);
        response[3] = await axios.get(`/users/userId=${users[3]}`);
    } catch (err) {
        console.log(err);
    }
}

از آنجایی که Async / Await بر پایه Promiseها ساخته شده است، می‌توانید از Promise.all() نیز به همراه کلمه کلیدی await استفاده کنید:

async function fetchUsers() {
  const user1 = getUser1();
  const user2 = getUser2();
  const user3 = getUser3();

  const results = await Promise.all([user1, user2, user3]);
}

نکته: Async / Await به علت طبیعت همگامش، کمی کندتر است. باید وقتی که از آن چندین بار در یک ردیف استفاده می‌کنید، مراقب باشید؛ زیرا کلمه کلیدی await اجرای کدهای بعد از خود را متوقف می‌کند.

چگونه استفاده از Async / Await را شروع کنیم؟

درک و استفاده از کار کردن با Async / Await، به طرز شگفت‌انگیزی ساده است. اگر می‌خواهید از سمت کاربر آن استفاده کنید، باید Babel را به کار بگیرید. Babel یک Transpiler با کاربری ساده برای وب است.

Async

بیایید با کلمه کلیدی async شروع کنیم. این کلمه می‌تواند به این صورت قبل از تابع قرار بگیرد:

async function returnTrue() {
  return true;
}

Await

کلمه کلیدی await، جاوااسکریپت را مجبور می‌کند تا وقتی که promise مورد نظر نتیجه‌اش را برگرداند، منتظر بماند. در اینجا یک مثال را می‌بینید:

let value = await promise; 

مثال کامل

// این تابع پس از ۱ ثانیه، مقدار «صحیح» را بر می‌گرداند
async function returnTrue() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve(true), 1000) // resolve
  });

  let result = await promise;

  // نتیجه (صحیح) را لاگ کن
  console.log(result);
}

// تابع را فراخوانی کن
returnTrue();

چرا async / await بهتر است؟

حال که همه چیز را درباره Promiseها و Async / Await بررسی کرده‌ایم، بیایید ببینیم که چرا Async / Await، انتخاب برتر برای کد ما است.

  1. Async / Await یک کد مرتب و خلاصه، با خط‌های کمتر، تایپ کردن کمتر و خطاهای کمتر را فراهم می‌کند. در نهایت نیز، کدهای پیچیده و تو در تو را خوانا می‌کند.
  2. مدیریت خطا با try / catch در یک جا، به جای انجام آن در تمام فراخوانی‌ها.
  3. درک مرجع بروز خطاها که در promiseها عظیم و پیچیده هستند، در Async / Await ساده‌تر است.

کلام آخر

به جرئت می‌توان گفت که Async / Await یکی از قوی‌ترین امکانات اضافه شده به JavaScript در سال‌های اخیر است. درک این سینتکس و استفاده از آن، کمتر از یک روز وقت می‌برد.

منبع

مقالات پیشنهادی

نکات مثبت، اشتباهات و نحوه استفاده async/await در جاوااسکریپت

async/await که در ES7 معرفی شد، بهبود شگفت‌انگیزی در برنامه‌نویسی ناهمگام با JavaScript است. این نوع تابع، گزینه‌ای برای استفاده از کد همگام برای دستر...

مدیریت خطا با استفاده از async / await و promiseها

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

شش طرح زیبا و الهام بخش از codepen

امروز میخوام چند طرح زیبا از codepen رو بهتون نشون بدم که کار آقای Karim Maaloul . کارهای ایشون با ابزار های مختلفی انجام شده و بسیار بسیار عالی و ز...

وب سایت های الهام بخش برای طراحی

امروز قصد داریم یک سری وبسایت های خارجی که بطور کاربردی ، زیبا و قدرتمند طراحی شدن رو براتون قرار بدیم تا شما بتونین با طریقه طراحی اونها آشنا بشین یا...