بررسی async و await در جاوااسکریپت

مدیریت عملیات ناهمگام با async/await رو توی این مقاله بررسی می‌کنیم.

سلام دوستان! توی پست امروز می‌خوایم یکی از مهمترین ویژگی‌های جاوااسکریپت که توسط ES8 (ES2017) معرفی شده یعنی توابع async رو بررسی کنیم.

این ویژگی برای مدیریت عملیات ناهمگام به کار میره و بهتره که قبلش بدونیم عملیات ناهمگام چی هست.


عملیات ناهمگام

یک سری عملیات توی جاوااسکریپت مثل اتصال به یک منبع خارجی با Ajax یا WebSocket و یا خوندن و نوشتن اطلاعات توی دیتابیس و هارددیسک، جدا از روند اصلی برنامه اجرا میشن.

به این عملیات میگن عملیات ناهمگام. یعنی وقتی مفسر به اون قسمت از کد رسید که مثلا داره با یک منبع خارجی ارتباط برقرار می‌کنه، منتظر نتیجه‌ی این قسمت نمی‌مونه و به ادامه روند برنامه می‌پردازه. یک مثال ساده از عملیات ناهمگام Ajax هست:

$.get('http://example.com/users', function(response) {
    console.log(`Don't wait for me!`);
});

console.log(`You see me sooner!`);

توی مثال بالا console.log توی خط ۵ زودتر از اونی که توی خط ۲ هست اجرا میشه.



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

استفاده از توابع Callback ظاهرا راحت هست. اما وقتی چند عملیات ناهمگام متوالی داشته باشیم چطور؟ یعنی برای مثال یک Ajax بعد از اجرا شدن یک Ajax دیگه:

$.get('http://example.com/users', function(res1) {
    $.get('http://example.com/posts?users=' + res1.users, function(res2) {

    });
});

خب اینجا یکم کار سخت میشه. چون وقتی یک یا چند تابع Callback درون هم دیگه قرار می‌گیرن، علاوه بر اینکه ساختار کدهای ما زشت میشن، کدها تو در تو میشن و کار با اطلاعات و متغیرها و نهایتا مدیریت کد سخت میشه که به این قضیه میگن جهنم کال‌بک (Callback Hell).


راه حل بهتر

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

بهتره قبل از بررسی توابع async با پرامیس‌ها آشنا بشیم. چون این توابع توی پشت پرده از پرامیس‌ها استفاده می‌کنن. (برای آشنایی کامل با پرامیس‌ها این مقاله اختصاصی رو بخونید)

مثال Ajax ابتدای پست رو با پرامیس‌ها می‌نویسیم:

let users = new Promise(function(resolve, reject) {
    $.get('http://example.com/users', function(response) {
        resolve(response);
    });
});


let posts = users.then(
    function(users) {
        return new Promise(function(resolve, reject) {
            $.get('http://example.com/posts?users' + users, function(posts) {
                resolve(posts);
            });
        });
    }
);

posts.then(
    function(user_posts) { 
        console.log(user_posts);
    }
);

توی خط اول ابتدا یک پرامیس نوشتیم که درخواست ‌Ajax اولی اجرا بشه. توی پرامیس‌ها، ما نتیجه‌ی عملیات رو می‌تونیم با متد then بررسی کنیم. متد then زمانی فراخونی میشه که داخل پرامیس resolve صدا زده بشه.

خب همونطور که می‌بینیم با پرامیس‌ها چند تا اتفاق خوب میوفته. اول اینکه نتیجه‌ی درخواست رو می‌تونیم هرجایی از کد بررسی کنیم (توی توابع کال‌بک مجبور بودیم همه چیز رو تو در تو بنویسیم). دوم اینکه برای یک پرامیس می‌تونیم بی‌نهایت then ضمیمه کنیم. همچنین کدهای ما مرتب‌تر و مدیریت نتیجه موفقیت‌آمیز بودن یا خطای عملیات ساختار یافته‌تر میشه.

بهتر از این هم هست؟ ? بله بله! هرچند پرامیس‌ها برای حل مشکل Callback Hell معرفی شدن و تا حدی زیادی هم موفق بودن، همونطور که می‌بینیم پیچیدگی خاص خودشون رو دارن. کدهای اضافی (Boilerplate code) و حجیم.


توابع Async

خب بعد از معرفی پیش‌زمینه‌ها، موضوع اصلی این پست اینجا شروع میشه. توابع Async توسط ES8 به جاوااسکریپت اومده و برای راحت‌تر کردن مدیریت عملیات ناهمگام استفاده میشه. توابع async زیر پوست خودشون از پرامیس‌ها استفاده می‌کنن. نحوه استفاده از اون خیلی راحت هست. برای استفاده از این ویژگی برای مدیریت یک عملیات ناهمگام، ابتدا از کلمه کلیدی async هنگام تعریف کردن یک تابع استفاده می‌کنیم. کلمه async رو همیشه ابتدای تعریف تابع می‌نویسیم:

async function users() {
    // ...
}

وقتی از async استفاده می‌کنیم، یک کلمه کلیدی دیگه هم داریم به اسم await. وقتی کلمه کلیدی await در ابتدای یک عبارت قرار بگیره، کد ما صبر می‌کنه تا خروجی این عبارت مشخص بشه و بعد به سراغ خط‌های بعدی میره. از await بصورت زیر استفاده می‌کنیم:

async function users() {
    let users = await getUsersFromServer();

    console.log(users);
}

تابع getUsersFromServer به فرض قراره با یک درخواست Ajax اطلاعاتی از کاربرا از سرور برای ما بفرسته. وقتی کلمه کلیدی await قبل از این عبارت قرار بگیره، کد صبر می‌کنه تا خروجی این قسمت مشخص بشه و بعد میره سراغ خط‌های بعدی. اگه از await استفاده نکنیم، خروجی خط ۴ کد بالا ممکنه زودتر از خط ۲ مشخص بشه.

یک نکته باید در نظر داشته باشیم که کلمه کلیدی await فقط باید درون تابعی استفاده بشه که از کلمه کلیدی async در ابتدای خودش استفاده می‌کنه. در غیر این صورت با خطا مواجه میشیم.

نکته‌ی بعدی که باید در نظر داشته باشیم اینه که اگه عبارت جلوی await یک پرامیس نباشه، بعد از اتمام کارش، این عبارت خود به خود تبدیل به یک پرامیس resolve شده میشه.

خروجی یک تابع async همیشه یک پرامیس هست و می‌تونیم مثل یک پرامیس با اون رفتار کنیم:

async function users() {
    let users = await getUsersFromServer();

    return users;
}

users.then(console.log); // list of users



کد زیر رو در نظر بگیرید. تابع numbersFromServer یک عملیات ناهمگام رو انجام میده که یک ثانیه طول میکشه و به فرض قراره یک سری اعداد رو از سرور بگیره (که من برای شبیه‌سازی از اعداد تصادفی استفاده کردم)

let numbersFromServer = () => {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            var numbers = randomNumbers();
            resolve(numbers);
        }, 1000);
    });
}

function randomNumbers() {
    return Array(5).fill().map(() => Math.round(Math.random() * 100))
}

اگه بخوایم خروجی تابع numbersFromServer رو با پرامیس‌ها پردازش کنیم کد ما شبیه به این میشه:

numbersFromServer()
    .then(numbers => numbers.sort())
    .then(sorted => sorted.reverse())
    .then(reversed => reversed.join())
    .then(console.log);

و اگه با توابع async بخوایم بررسی کنیم:

async function render() {
    let numbers = await numbersFromServer();

    var sorted = numbers.sort();
    var reversed = sorted.reverse();
    var joined = reversed.join();

    console.log(joined);
}

render();

همونطور که می‌بینیم کدهای ما با async در مقایسه با پرامیس‌ها که باید از متدهای زنجیره‌ای استفاده می‌کردیم، ساده‌تر و خواناتر شدن.




خب ابتدای این پست از مشکلات توابع Callback گفتیم که وقتی تو در تو و زیاد بشن مشکلی به اسم Callback Hell درست می‌کنن. توی چنین شرایطی از توابع async استفاده می‌کنیم. ابتدا توابعی که توی مثال اول پست تو در تو بودن رو بصورت مجزا تعریف می‌کنیم. توی کد زیر تابع users یک سری کاربرا رو از سرور می‌گیره. همچنین تابع grades به عنوان آرگومان یک لیست از کاربرا رو می‌گیره و نمرات اونها رو برمی‌گردونه:

let users = () => {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            var users = ['John', 'Donald', 'Sarah'];
            resolve(users);
        }, 1000);
    });
}

// ...

let grades = (users) => {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('Grades for: ' + users.join());
            var grades = [1, 2, 3, 4];
            resolve(grades);
        }, 1000);
    });
}

حالا با استفاده از یک تابع async از این توابع استفاده می‌کنیم:

async function render() {
    let usersFromServer = await users();
    let gradesFromServer = await grades(usersFromServer);
    
    var sorted = gradesFromServer.sort();
    var reversed = sorted.reverse();
    var joined = reversed.join();

    console.log(joined);
}

render();

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

خب دوستان امیدوارم از این پست استفاده کرده باشین. منتظر نظراتتون هستم. موفق باشین ✌️?




Resources :

https://flaviocopes.com/javascript-async-await/

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

ditty