در این مقاله قصد داریم تا درمورد نحوه نوشتن فراخوانیهای تابع ناهمگام declarative با استفاده از promise ها در جاوااسکریپت صحبت کنیم. همچنین بررسی خواهیم کرد که این موضوع چگونه میتواند به خوانایی بیشتر کد ما و نگهداری آسانتر آن کمک کند.
جاوااسکریپت یک زبان برنامه نویسی single-thread است. یعنی این که در یک زمان فقط میتواند کد را بهصورت همگام و یا به ترتیب از بالا به پایین اجرا کند. با این حال برنامه نویسی ناهمگام برای رفع این مشکل معرفی شده است.
مفهوم برنامه نویسی ناهمگام جز مفاهیم اصلی در جاوااسکریپت است و یک تابع را قادر میسازد تا در حالی که منتظر پایان یافتن اجرای سایر توابع است، اجرا شود. برای برقراری فراخوانیهای API به بکاند از توابع ناهمگام استفاده میکنیم. همچنین برای نوشتن در یک فایل یا پایگاه داده و یا خواندن آن هم این توابع مورد استفاده قرار میگیرند. بهطور کلی این مفهوم هم برای توسعهدهندگان سمت سرور و هم برای توسعهدهندگان سمت کلاینت مفید است.
قبل از اینکه وارد بحث کدنویسی شویم ابتدا الگوی برنامه نویسی declarative را مرور میکنیم.
برنامه نویسی declarative یک نوعی از برنامه نویسی است که بهطور کلی منطق کد را بیان میکند اما مراحلی که برای رسیدن به آن دنبال میشود را نشان نمیدهد. در این نوع برنامه نویسی به شکل دقیق مشخص نیست که در پسزمنیه چه اتفاقاتی در حال انجام شدن است.
برعکس، برنامه نویسی imperative نیازمند نوشتن کد بهصورت گام به گام است که هر مرحله جزئیات به شکل دقیق توضیح داده شود. این کار میتواند زمینه مفیدی برای توسعهدهندگان آینده، که ممکن است نیاز به کار با کد داشته باشند فراهم کند اما باعث میشود کد بسیار طولانی شود. برنامه نویسی imperative اغلب غیرضروری است و این موضوع تماما به هدفی که ما داریم وابسته است.
میتوانیم برنامه نویسی declarative را با استفاده از روشهای جاوااسکریپت انجام دهیم. این نوع برنامه نویسی به ما کمک میکند تا کدی بنویسیم که خواناتر باشد و در نتیجه درک آن آسانتر شود. به عنوان مثال، در این نوع برنامه نویسی ما نیازی به استفاده از حلقه for برای کار کردن با آرایه نداریم. در عوض میتوانیم به سادگی از روشهای آرایه داخل جاوااسکریپت مانند map()
،reduce()
و forEach()
استفاده کنیم.
در اینجا یک مثال برنامه نویسی imperative آوردهایم که تابعی را نشان میدهد که یک رشته را با استفاده از یک حلقه for
کاهشیابنده، معکوس میکند:
const reverseString = (str) => { let reversedString = ""; for (var i = str.length - 1; i >= 0; i--) { reversedString += str[i]; } return reversedString; }
اما سوالی که وجود دارد این است که چرا باید برای انجام این کار ده خط کد بنویسیم در حالی که میتوانیم تنها با دو خط کد به همان راه حل برسیم؟
در ادامه یک نسخه برنامه نویسی declarative از همان کد، با استفاده از روشهای آرایه داخل جاوااسکریپت آمده است:
const reverseString = (str) => { return str.split("").reverse().join(""); }
این مثال از دو خط کد برای معکوس کردن یک رشته استفاده میکند که هم خیلی کوتاه است و هم مستقیماً به مفهوم اصلی میرسد.
promise یک آبجکت جاوااسکریپتی است که نتایج یک تابع ناهمگام را شامل میشود. به عبارت دیگر، taskای را نشان میدهد که در یک تابع ناهمگام تکمیل شده است و یا اینکه ناموفق بوده است.
const promise = new Promise (function (resolve, reject) { // code to execute })
تابع سازنده promise
یک آرگومان میگیرد، یک تابع callback که به آن executor نیز میگویند. تابع executor نیز دو تابع callback میگیرد: resolve
و reject
. اگر تابع executor با موفقیت اجرا شود، متدresolve()
فراخوانی میشود و حالت promise
از «در حال انتظار» به «انجام شده» تغییر پیدا میکند. اما اگر تابع executor با شکست مواجه شود، متد reject()
فراخوانی میشود و در این صورت حالت promise
از «در حال انتظار» به «ناموفق» تغییر میکند.
همانطور که در مثال زیر نشان داده شده است از متد.then ()
استفاده میکنیم تا promise وارد مرحله اجرا شود و به این ترتیب به مقدار resolve شده دسترسی پیدا کنیم:
promise.then(resolvedData => { // do something with the resolved value })
به طور مشابه، در مورد یک مقدار رد شده نیز از متدcatch()
استفاده میکنیم:
promise.then(resolvedData => { // do something with the resolved value }).catch(err => { // handle the rejected value })
وقتی چندین تابع callback یا متد .then
تودرتو داریم، اغلب نگهداری کد و حفظ خوانایی آن مشکل میشود.
کلمه کلیدی async
به ما کمک میکند تا توابعی را که در جاوااسکریپت عملیات ناهمگام انجام میدهند، تعریف کنیم. در همین حال، کلمه کلیدی await
به موتور جاوااسکریپت دستور میدهد تا قبل از بازگرداندن نتایج، منتظر تکمیل تابع بماند.
سینتکس async/wait یک سینتکس ساده برای promise ها در جاوااسکریپت است. این موضوع به ما کمک میکند تا به کد تمیزتری دست پیدا کنیم که نگهداری آن نیز آسانتر است.
const getUsers = async () => { const res = await fetch('https://jsonplaceholder.typicode.com/users'); const data = await res.json(); return data; }
Async/wait درواقع promise ها یا توابع ناهمگام در جاوااسکریپت را قادر میسازد تا بهصورت همگام اجرا شوند. با این حال، توصیه میشود که همیشه کلمه کلیدی await
را داخل یک بلاک try…catch قرار دهیم تا به این طریق از خطاهای غیرمنتظره جلوگیری کنیم.
در ادامه مثالی داریم که در آن کلمه کلیدی await
و تابع getUsers()
را در داخل یک بلاک try...catch
قرار دادهایم:
const onLoad = async () => { try { const users = await getUsers(); // do something with the users } catch (err) { console.log(err) // handle the error } }
یکی از دلایلی که باعث شده است تا async/wait یک ویژگی فوقالعاده در جاوااسکریپت مدرن باشد این است که به ما کمک میکند تا با مشکلات توابع callback مواجه نشویم.
با این حال، بررسی خطاهای چندین تابع async
میتواند چیزی شبیه به مثال پایین باشد:
try { const a = await asyncFuncOne(); } catch (errA) { // handle error } try { const b = await asyncFunctionTwo(); } catch (errB) { // handle error } try { const c = await asyncFunctionThree(); } catch (errC) { // handle error }
اگر همه توابع async
را به یک بلاک try
اضافه کنیم در نهایت چند شرط if را در بلاک catch
خود خواهیم نوشت، زیرا بلاک catch
اکنون حالت عمومیتری دارد:
try { const a = await asyncFuncOne(); const b = await asyncFunctionTwo(); const c = await asyncFunctionThree(); } catch (err) { if(err.message.includes('A')) { // handle error for asyncFuncOne } if(err.message.includes('B')) { // handle error for asyncFunctionTwo } if(err.message.includes('C')) { // handle error for asyncFunctionThree } }
این موضوع باعث میشود حتی با استفاده از دستور async/wait، قابلیت خوانایی کد پایینتر بیاید و نگهداری آن دشوارتر شود.
برای حل این مشکل، میتوانیم یک تابع utility بنویسیم که شامل promise میشود و از بلاکهای تکراری try...catch
جلوگیری میکند.
تابع utility یک promise را به عنوان پارامتر دریافت میکند، خطا را به صورت داخلی مدیریت کرده و یک آرایه که شامل دو عنصر است برمیگرداند: مقدار resolve شده و مقدار rejecte شده.
تابع، promise را resolve کرده و دادهها را در اولین عنصر آرایه برمیگرداند. همینطور خطا در عنصر دوم آرایه برگردانده میشود. اگر promise در مرحله قبل resolve شده باشد، عنصر دوم به عنوان مقدار null بازگشت داده میشود.
const promiser = async (promise) => { try { const data = await promise; return [data, null] } catch (err){ return [null, error] } }
ما میتوانیم کد بالا را اصلاح کرده و بلاک try...catch
را با برگرداندن promise
با استفاده از متدهای .then()
و .catch()
حذف کنیم:
const promiser = (promise) => { return promise.then((data) => [data, null]).catch((error) => [null, error]); };
همچنین میتوانیم کاربرد تابع utility را در ادامه مشاهده کنیم:
const demoPromise = new Promise((resolve, reject) => { setTimeout(() => { // resolve("Yaa!!"); reject("Naahh!!"); }, ۵۰۰۰); }); const runApp = async () => { const [data, error] = await promiser(demoPromise); if (error) { console.log(error); return; } // do something with the data }; runApp();
در ادامه یک مثال در دنیای واقعی را بررسی خواهیم کرد. در مثال پایین، تابع generateShortLink
از یک سرویس کوتاه کننده URL برای خلاصه کردن یک URL کامل استفاده میکند.
اینجا تابع promiser()
متد axios.get ()
را دربرمیگیرد تا پاسخ را از سرویس کوتاه کننده URL بازگرداند.
import promiser from "./promise-wrapper"; import axios from "axios"; const generateShortLink = async (longUrl) => { const [response, error] = await promiser( axios.get(`https://api.1pt.co/addURL?long=${longUrl}`) ); if (error) return null; return `https://1pt.co/${response.data.short}`; };
برای مقایسه بهتر، نحوه عملکرد تابع promiser()
بدون wrapper در ادامه مطرح شده است :
const generateShortLink = async (longUrl) => { try { const response = await axios.get( `https://api.1pt.co/addURL?long=${longUrl}` ); return `https://1pt.co/${response.data.short}`; } catch (err) { return null; } };
اکنون برای این که مثال را تکمیل کنیم فرمی میسازیم که از متد geneShortLink()
استفاده میکند:
<!DOCTYPE html> <html> <head> <title>Demo</title> <meta charset="UTF-8" /> </head> <body> <div id="app"> <form id="shortLinkGenerator"> <input type="url" id="longUrl" /> <button>Generate Short Link</button> </form> <div id="result"></div> </div> <script src="src/index.js"></script> </body> </html>
تا این مرحله، تابع promiser()
میتواند تنها یک تابع async
را در خود داشته باشد. با این حال، اکثر use caseها نیاز دارند تا چندین تابع مستقل و ناهمگام را مدیریت کنند.
برای بررسی بسیاری از promiseها، میتوانیم از متد Promise.all()
استفاده کنیم و آرایهای از توابع async
را به تابع promiser
ارسال کنیم:
const promiser = (promise) => { if (Array.isArray(promise)) promise = Promise.all(promise); return promise.then((data) => [data, null]).catch((error) => [null, error]); };
در ادامه یک مثال از تابع promiser()
که با چندین تابع async
مورد استفاده قرار میگیرد، مطرح کردهایم:
const categories = ["science", "sports", "entertainment"]; const requests = categories.map((category) => axios.get(`https://inshortsapi.vercel.app/news?category=${category}`) ); const runApp = async () => { const [data, error] = await promiser(requests); if (error) { console.error(error?.response?.data); return; } console.log(data); }; runApp();
راه حلهایی که درمورد آنها صحبت کردیم برای نوشتن فراخوانیهای تابع ناهمگام declarative در جاوااسکریپت برای اکثر سناریوها ایدهآل هستند. با این حال، موارد استفاده دیگری نیز وجود دارد که ممکن است لازم باشد تا آنها را هم در نظر بگیریم. به عنوان مثال ممکن است بخواهیم فقط خطاهای مورد انتظار را مدیریت کنیم و هر خطای استثنایی را که در طول اجرای promise در جاوااسکریپت رخ میدهد، از بین ببریم.
همچنین مطالبی که در این مقاله مطرح شد نقطه ورود خوبی برای ایجاد APIهای پیچیدهتر و توابع کاربردی بهشمار میرود.