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

جاوااسکریپت و برنامه نویسی ناهمگام

جاوااسکریپت یک زبان برنامه نویسی single-thread است. یعنی این که در یک زمان فقط می‌تواند کد را به‌صورت همگام و یا به ترتیب از بالا به پایین اجرا کند. با این حال برنامه نویسی ناهمگام برای رفع این مشکل معرفی شده است.

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

برنامه نویسی declarative

قبل از اینکه وارد بحث کدنویسی شویم ابتدا الگوی برنامه نویسی 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ها در جاوااسکریپت

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
})

Async/await

وقتی چندین تابع 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
  }
}

سفارشی‌سازی یک wrapper برای promise در جاوااسکریپت

یکی از دلایلی که باعث شده است تا 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های پیچیده‌تر و توابع کاربردی به‌شمار می‌رود.

 

منبع