بارگذاری غیرهمزمان جاوا اسکریپت | افزایش سرعت سایت

بارگذاری غیرهمزمان جاوا اسکریپت | افزایش سرعت سایت

استفاده از تکنیک های بارگذاری غیرهمزمان JavaScript

برای ایجاد وب سایت ها و برنامه های کاربردی واکنش گرا و سریع، تسلط بر تکنیک های بارگذاری غیرهمزمان JavaScript ضروری است. این تکنیک ها به توسعه دهندگان کمک می کنند تا عملیات زمان بر را بدون مسدود کردن رابط کاربری مدیریت کنند و تجربه ای روان تر برای کاربران فراهم آورند. این مقاله سفری عمیق به دنیای Callbacks، Promises و Async/Await است تا راهکارهای بهینه سازی عملکرد وب را کشف کنیم و کدهایی خواناتر و کارآمدتر بنویسیم.

چرا برنامه نویسی غیرهمزمان در جاوا اسکریپت ضروری است؟

در دنیای پرسرعت وب امروز، انتظارات کاربران از سرعت و واکنش گرایی یک وب سایت بسیار بالا رفته است. هر تأخیری، هر چند کوتاه، می تواند به از دست دادن کاربر و کاهش کیفیت تجربه او منجر شود. جاوا اسکریپت به عنوان زبان اصلی سمت کلاینت (و اخیراً سمت سرور با Node.js)، نقش حیاتی در تعامل پذیری و پویایی وب سایت ها ایفا می کند. اما این زبان ذاتاً تک رشته ای (Single-threaded) است؛ به این معنا که تنها می تواند یک عملیات را در هر زمان اجرا کند.

این ویژگی تک رشته ای بودن در نگاه اول ساده به نظر می رسد، اما چالش های بزرگی را در برنامه نویسی وب مدرن ایجاد می کند. تصور کنید یک برنامه جاوا اسکریپت در حال بارگذاری یک فایل بزرگ، درخواست داده از یک API، یا انجام محاسبات پیچیده است. اگر این عملیات به صورت همگام (Synchronous) انجام شوند، رشته اصلی (Main Thread) برنامه برای مدت زمانی مسدود می شود. این انسداد به معنای فریز شدن رابط کاربری (UI) است؛ کاربر نمی تواند با صفحه تعامل داشته باشد، دکمه ها کار نمی کنند، انیمیشن ها متوقف می شوند و وب سایت به نظر می رسد که از کار افتاده است.

اینجاست که برنامه نویسی ناهمگام (Asynchronous JavaScript Programming) وارد میدان می شود. تکنیک های ناهمگام به ما این امکان را می دهند که عملیات زمان بر را در پس زمینه آغاز کنیم و به رشته اصلی اجازه دهیم تا به پاسخگویی به تعاملات کاربر و به روزرسانی UI ادامه دهد. نتیجه نهایی، یک رابط کاربری همواره واکنش گرا، کارایی بالاتر در بارگذاری منابع و استفاده بهینه از توان پردازشی است. با تسلط بر این تکنیک ها، می توانیم وب سایت ها و اپلیکیشن هایی خلق کنیم که کاربران از کار با آن ها لذت ببرند.

پیش نیازها: برای شروع این سفر چه باید دانست؟

قبل از اینکه به عمق مفاهیم بارگذاری غیرهمزمان JavaScript شیرجه بزنیم، داشتن دانش پایه ای در مورد جاوا اسکریپت به شما کمک شایانی خواهد کرد. درک مفاهیمی مانند توابع (Functions)، متغیرها (Variables) و دامنه (Scope) در جاوا اسکریپت اهمیت بالایی دارد. این ها بلوک های سازنده ای هستند که تمام کد جاوا اسکریپت بر روی آن ها بنا شده است.

علاوه بر این، آشنایی اولیه با مدل شیء سند (DOM – Document Object Model) و نحوه تعامل جاوا اسکریپت با آن ضروری است. بسیاری از عملیات ناهمگام در نهایت با دستکاری DOM سروکار دارند؛ به عنوان مثال، بارگذاری داده ها از سرور و نمایش آن ها بر روی صفحه نیازمند فهم این است که چگونه می توان عناصر جدیدی را به HTML اضافه کرد یا محتوای موجود را تغییر داد. با این پیش نیازها، آماده اید تا مفاهیم پیچیده تر برنامه نویسی ناهمگام را با اعتمادبه نفس بیشتری دنبال کنید.

جاوا اسکریپت همگام: محدودیت های یک دنیای خطی

برای درک کامل زیبایی و کارایی برنامه نویسی ناهمگام جاوا اسکریپت، ابتدا باید با برادر سنتی تر و خطی تر آن، یعنی جاوا اسکریپت همگام (Synchronous JavaScript) آشنا شویم. جاوا اسکریپت همگام، کدی را خط به خط و دقیقاً به ترتیبی که نوشته شده است، اجرا می کند. هر عملیاتی باید به طور کامل به پایان برسد تا عملیات بعدی بتواند آغاز شود. این فرایند اغلب به عنوان مسدودکننده (Blocking) توصیف می شود؛ زیرا رشته اصلی برنامه را برای انجام هر کار دیگری تا زمان تکمیل عملیات فعلی، مسدود می کند.

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

Callback ها: اولین تجربه از دنیای ناهمگامی

مفهوم و کارکرد Callback ها

Callback ها اولین و شاید ساده ترین گام در مسیر برنامه نویسی ناهمگام جاوا اسکریپت بودند. یک Callback، در اصل تابعی است که به عنوان آرگومان به تابع دیگری فرستاده می شود و قرار است پس از اتمام یک عملیات خاص، اجرا شود. تصور کنید شما به یک دوست می گویید: وقتی این کار را تمام کردی، این کار را برای من انجام بده. تابع اصلی، کار خودش را شروع می کند و وقتی به اتمام رسید، Callback را فراخوانی (Call back) می کند تا عملیات بعدی را انجام دهد.

این مکانیزم به جاوا اسکریپت اجازه می دهد تا بدون انتظار برای تکمیل عملیات زمان بر، به اجرای کدهای بعدی ادامه دهد. وقتی عملیات اصلی به نتیجه رسید (مثلاً داده ها از سرور دریافت شد)، Callback اجرا می شود و نتایج را پردازش می کند. این رویکرد، پایه و اساس بسیاری از تعاملات وب، از پاسخگویی به کلیک های کاربر گرفته تا بارگذاری داده ها از طریق شبکه است.

مثال های عملی از Callbacks

Callback ها در بسیاری از توابع و API های رایج جاوا اسکریپت حضور دارند:

  • setTimeout و setInterval: این توابع برای زمان بندی اجرای یک تابع در آینده یا به صورت مکرر استفاده می شوند. تابعی که به آن ها می دهید، یک Callback است که پس از مدت زمان مشخصی اجرا می شود.
  • addEventListener: این متد به شما امکان می دهد تا به رویدادهای مختلف DOM (مانند کلیک کردن، حرکت ماوس یا ارسال فرم) گوش دهید. تابعی که به addEventListener می دهید، Callback است که هر بار رویداد مورد نظر رخ می دهد، اجرا می شود.
  • XMLHttpRequest (XHR): این API قدیمی تر، اما هنوز کاربردی، برای ارسال درخواست های HTTP به سرور و دریافت پاسخ استفاده می شود. Callback ها برای مدیریت موفقیت یا شکست درخواست ها به کار می روند.

function fetchData(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.onload = function() {
    if (xhr.status === 200) {
      callback(null, JSON.parse(xhr.responseText));
    } else {
      callback(new Error('خطا در بارگذاری داده: ' + xhr.status), null);
    }
  };
  xhr.onerror = function() {
    callback(new Error('خطای شبکه رخ داد.'), null);
  };
  xhr.send();
}

fetchData('https://api.example.com/data', function(error, data) {
  if (error) {
    console.error(error.message);
  } else {
    console.log('داده ها دریافت شد:', data);
  }
});

چالش Callback Hell و مدیریت ابتدایی خطا

با وجود سادگی و کاربردی بودن Callback ها، زمانی که چندین عملیات ناهمگام باید به صورت متوالی و وابسته به یکدیگر اجرا شوند، با مشکلی به نام Callback Hell یا جهنم Callback مواجه می شویم. این وضعیت زمانی رخ می دهد که Callback ها به صورت تو در تو و با فرورفتگی های زیاد (indentation) نوشته می شوند و کد را بسیار دشوار برای خواندن، درک و نگهداری می کنند. تصور کنید برای بارگذاری یک کاربر، سپس پست های او، و سپس نظرات هر پست، باید سه یا چهار Callback تو در تو بنویسیم. این کد به سرعت به یک هرم مرگ تبدیل می شود.

Callback Hell یا هرم مرگ، چالش اصلی در برنامه نویسی ناهمگام با استفاده از Callback هاست که خوانایی و نگهداری کد را به شدت کاهش می دهد.

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

Promise ها: وعده هایی برای آینده ای سازمان یافته

معرفی Promise و حل مشکلات Callback

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

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

مفاهیم اساسی Promise

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

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

Promise ها همچنین شامل دو بخش اصلی هستند: تولید کننده (Producer) که کار ناهمگام را انجام می دهد و نتیجه را مشخص می کند، و مصرف کننده (Consumer) که منتظر نتیجه می ماند و با آن کار می کند. این جدایی مسئولیت ها باعث سازمان یافته تر شدن کد می شود.

ایجاد و استفاده از Promise ها

یک Promise با استفاده از سازنده new Promise() ساخته می شود که یک تابع به عنوان آرگومان می گیرد. این تابع دو آرگومان دیگر به نام های resolve و reject دارد. resolve برای نشان دادن موفقیت و reject برای نشان دادن شکست عملیات استفاده می شود.


const myPromise = new Promise((resolve, reject) => {
  // شبیه سازی یک عملیات ناهمگام
  setTimeout(() => {
    const success = true; // یا false برای شبیه سازی خطا
    if (success) {
      resolve('عملیات با موفقیت انجام شد!');
    } else {
      reject(new Error('عملیات با خطا مواجه شد.'));
    }
  }, 2000);
});

برای استفاده از نتیجه یک Promise، از متدهای زیر استفاده می کنیم:

  • .then(): این متد برای رسیدگی به حالت موفقیت آمیز Promise استفاده می شود. یک Callback دریافت می کند که وقتی Promise با موفقیت انجام شد، با مقدار حاصل از resolve اجرا می شود.
  • .catch(): این متد برای مدیریت خطاها در Promise استفاده می شود. یک Callback دریافت می کند که وقتی Promise رد شد، با مقدار حاصل از reject اجرا می شود.
  • .finally(): این متد، Callback ای را دریافت می کند که همیشه، چه Promise موفق شود و چه شکست بخورد، اجرا می شود. معمولاً برای عملیات نهایی مانند پنهان کردن لودر استفاده می شود.

myPromise
  .then((message) => {
    console.log(message); // 'عملیات با موفقیت انجام شد!'
  })
  .catch((error) => {
    console.error(error.message); // 'عملیات با خطا مواجه شد.'
  })
  .finally(() => {
    console.log('عملیات Promise به پایان رسید.');
  });

زنجیره سازی Promise ها (Promise Chaining)

یکی از بزرگترین مزایای Promise ها، قابلیت زنجیره سازی Promise ها (Promise Chaining) است. هر متد .then() یک Promise جدید برمی گرداند، که به شما امکان می دهد چندین عملیات ناهمگام را به صورت متوالی و خوانا اجرا کنید. نتیجه هر .then() به .then() بعدی منتقل می شود و این کار به جلوگیری از Callback Hell کمک می کند.


fetch('https://api.example.com/users')
  .then(response => response.json()) // تبدیل پاسخ به JSON
  .then(users => {
    console.log('لیست کاربران:', users);
    return fetch(`https://api.example.com/users/${users[0].id}/posts`); // درخواست پست های اولین کاربر
  })
  .then(response => response.json())
  .then(posts => {
    console.log('پست های اولین کاربر:', posts);
  })
  .catch(error => {
    console.error('خطا در زنجیره Promise:', error);
  });

مثال عملی: بارگذاری داده ها با fetch API و Promise ها

fetch API یک روش مدرن و قدرتمند برای بارگذاری داده در JavaScript از طریق شبکه است که به طور پیش فرض Promise برمی گرداند. این API جایگزینی کارآمدتر برای XMLHttpRequest محسوب می شود و به طور گسترده در توسعه وب واکنش گرا استفاده می گردد.


fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then(todo => {
    console.log('Todo دریافت شده:', todo);
    // نمایش در DOM
    const p = document.createElement('p');
    p.textContent = `عنوان: ${todo.title}, تکمیل شده: ${todo.completed ? 'بله' : 'خیر'}`;
    document.body.appendChild(p);
  })
  .catch(error => {
    console.error('خطا در واکشی Todo:', error);
  });

ترکیب کننده های Promise (Promise Combinators) برای عملیات موازی

گاهی اوقات نیاز داریم چندین Promise را به صورت موازی اجرا کنیم و نتایج آن ها را به صورت یکجا مدیریت نماییم. Promise Combinators ابزارهایی هستند که این کار را آسان می کنند:

  • Promise.all(): این متد آرایه ای از Promise ها را می گیرد و یک Promise جدید برمی گرداند که زمانی که تمام Promise های ورودی موفقیت آمیز باشند، با آرایه ای از نتایج آن ها انجام می شود. اگر حتی یک Promise رد شود، Promise.all() بلافاصله رد می شود. این ابزار برای بارگذاری همزمان چندین تصویر یا فایل بسیار کاربردی است.
  • Promise.race(): نیز آرایه ای از Promise ها را می گیرد و یک Promise جدید برمی گرداند که به محض اینکه اولین Promise از میان آن ها (چه موفق و چه شکست خورده) به نتیجه برسد، با همان نتیجه انجام یا رد می شود. برای سناریوهایی که اولین پاسخ اهمیت دارد (مثلاً از چند سرور) مفید است.
  • Promise.allSettled(): این متد آرایه ای از Promise ها را می گیرد و یک Promise برمی گرداند که پس از تمام Promise های ورودی (چه موفق و چه شکست خورده) به نتیجه می رسد. نتیجه آن یک آرایه از اشیاء است که وضعیت و مقدار/دلیل هر Promise را مشخص می کند. برخلاف Promise.all()، حتی اگر برخی از Promise ها شکست بخورند، متوقف نمی شود.
  • Promise.any(): آرایه ای از Promise ها را می گیرد و یک Promise جدید برمی گرداند که به محض اینکه اولین Promise از میان آن ها با موفقیت انجام شود، با همان نتیجه انجام می شود. اگر تمام Promise ها رد شوند، با یک خطای AggregateError رد می شود. این متد برای مواقعی مفید است که می خواهید حداقل یکی از عملیات ها موفق شود.

const promise1 = Promise.resolve(3);
const promise2 = 42; // مقدار مستقیم نیز می تواند باشد
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log('نتایج Promise.all:', values); // [3, 42, foo]
});

const p1 = new Promise((resolve, reject) => setTimeout(resolve, 500, 'اولین'));
const p2 = new Promise((resolve, reject) => setTimeout(resolve, 100, 'دومین'));

Promise.race([p1, p2]).then((value) => {
  console.log('نتیجه Promise.race:', value); // 'دومین' (چون زودتر حل شد)
});

با استفاده از این ترکیب کننده ها، توسعه دهندگان می توانند سناریوهای پیچیده تری از بارگذاری غیرهمزمان JavaScript را با انعطاف پذیری و کنترل بیشتری مدیریت کنند.

Async/Await: کد ناهمگام به زیبایی همگام

مقدمه ای بر Async/Await: انقلابی در خوانایی کد

با معرفی Async/Await در جاوا اسکریپت در ES2017، برنامه نویسی ناهمگام به سطح جدیدی از خوانایی و سادگی رسید. Async/Await بر پایه Promise ها ساخته شده است، اما کدی را که شامل عملیات ناهمگام است، به گونه ای می نویسد که بسیار شبیه به کد همگام به نظر می رسد. این ویژگی، درک و دیباگ کردن کد را برای توسعه دهندگان به شدت آسان تر می کند و به عنوان یک انقلاب در نحوه مدیریت کد ناهمگام شناخته می شود.

با Async/Await، دیگر نیازی به استفاده مستقیم از .then() و .catch() برای هر Promise نداریم، بلکه می توانیم از ساختار try...catch آشنا برای مدیریت خطا استفاده کنیم. این رویکرد، کد را نه تنها خواناتر می کند، بلکه به ما کمک می کند تا جریان منطقی برنامه را به شکل طبیعی تری دنبال کنیم و از پیچیدگی های مرتبط با Callback Hell یا زنجیره های طولانی Promise اجتناب کنیم. در نهایت، هدف نهایی Async/Await، ارائه یک راهکار قدرتمند و در عین حال ساده برای استفاده از تکنیک های بارگذاری غیرهمزمان JavaScript است.

کلمات کلیدی async و await

Async/Await بر اساس دو کلمه کلیدی اصلی کار می کند:

  • async: این کلمه کلیدی قبل از تعریف یک تابع قرار می گیرد و آن را به یک تابع غیرهمزمان تبدیل می کند. یک تابع async همواره یک Promise برمی گرداند. اگر تابع یک مقدار غیر Promise برگرداند، جاوا اسکریپت به طور خودکار آن را در یک Promise حل شده (resolved Promise) بسته بندی می کند.
  • await: این کلمه کلیدی تنها می تواند در داخل یک تابع async استفاده شود. await اجرای تابع async را متوقف می کند تا Promise ای که جلوی آن قرار گرفته است، حل یا رد شود. پس از حل شدن Promise، مقدار حاصل از آن به عنوان نتیجه await بازگردانده می شود و اجرای تابع async از سر گرفته می شود.

async function fetchUserData() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
  const user = await response.json();
  console.log('اطلاعات کاربر:', user);
  return user;
}

fetchUserData();

مدیریت خطا با try...catch در Async/Await

یکی از بزرگترین مزایای Async/Await، امکان استفاده از بلوک های try...catch برای مدیریت خطا است که رویکردی بسیار آشنا و سرراست برای برنامه نویسان فراهم می کند. در Promise ها، خطاها با .catch() مدیریت می شدند، اما با async/await، می توانیم عملیات ناهمگام را درون بلوک try قرار دهیم و هرگونه خطایی که در طول اجرای await رخ دهد را با catch بگیریم.


async function getUserDataSafe() {
  try {
    const response = await fetch('https://invalid.url/users'); // آدرس نامعتبر برای شبیه سازی خطا
    if (!response.ok) {
      throw new Error(`خطای HTTP! وضعیت: ${response.status}`);
    }
    const user = await response.json();
    console.log('اطلاعات کاربر:', user);
  } catch (error) {
    console.error('خطا در دریافت اطلاعات کاربر:', error.message);
  }
}

getUserDataSafe();

مثال عملی و مقایسه ای: Async/Await در عمل

برای درک بهتر قدرت Async/Await، اجازه دهید یک مثال عملی را با روش های Callback، Promise و Async/Await مقایسه کنیم. فرض کنید می خواهیم داده های یک کاربر و سپس پست های او را از یک API دریافت کنیم.


// 1. با Callback ها (مثال ساده و فرضی)
function fetchUserCallback(userId, callback) {
  setTimeout(() => {
    const user = { id: userId, name: 'آرش' };
    callback(user);
  }, 1000);
}

function fetchUserPostsCallback(userId, callback) {
  setTimeout(() => {
    const posts = [{ id: 1, title: 'پست اول' }];
    callback(posts);
  }, 1000);
}

fetchUserCallback(1, (user) => {
  console.log('Callback User:', user);
  fetchUserPostsCallback(user.id, (posts) => {
    console.log('Callback Posts:', posts);
  });
});

// 2. با Promise ها
function fetchUserPromise(userId) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ id: userId, name: 'آرش' });
    }, 1000);
  });
}

function fetchUserPostsPromise(userId) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve([{ id: 1, title: 'پست اول' }]);
    }, 1000);
  });
}

fetchUserPromise(1)
  .then(user => {
    console.log('Promise User:', user);
    return fetchUserPostsPromise(user.id);
  })
  .then(posts => {
    console.log('Promise Posts:', posts);
  })
  .catch(error => console.error('Promise Error:', error));

// 3. با Async/Await
async function fetchUserAndPostsAsync(userId) {
  try {
    const user = await fetchUserPromise(userId);
    console.log('Async/Await User:', user);
    const posts = await fetchUserPostsPromise(user.id);
    console.log('Async/Await Posts:', posts);
  } catch (error) {
    console.error('Async/Await Error:', error);
  }
}

fetchUserAndPostsAsync(1);

همانطور که مشاهده می شود، نسخه Async/Await از نظر خوانایی و شباهت به کد همگام، به طور چشمگیری برتر است. این سادگی به توسعه دهندگان کمک می کند تا با مدیریت خطا در کد ناهمگام به شکلی طبیعی تر کنار بیایند و جریان برنامه را به راحتی درک کنند. این سهولت باعث افزایش سرعت وب سایت با جاوا اسکریپت ناهمگام و ارتقاء تجربه کاربر می شود.

محدودیت ها و نکات استفاده صحیح

گرچه Async/Await ابزاری قدرتمند است، اما مهم است که از محدودیت ها و نکات استفاده صحیح آن آگاه باشیم:

  • فقط داخل توابع async: کلمه کلیدی await تنها در داخل توابع تعریف شده با async قابل استفاده است. شما نمی توانید در سطح بالای یک اسکریپت (Global Scope) به صورت مستقیم از await استفاده کنید، مگر اینکه در محیط هایی مانند ماژول های ES (ES Modules) یا ابزارهای باندلر که این قابلیت را پشتیبانی می کنند، باشید.
  • اجرای موازی: استفاده بیش از حد از await به صورت متوالی می تواند باعث کندی شود، زیرا هر await منتظر تکمیل Promise قبلی می ماند. برای اجرای عملیات مستقل به صورت موازی، باید از Promise.all() یا سایر ترکیب کننده های Promise در کنار await استفاده کنید.
  • عدم مسدود کردن رشته اصلی: await رشته اصلی جاوا اسکریپت را مسدود نمی کند. بلکه فقط اجرای تابع async را تا زمان حل شدن Promise متوقف می کند و اجازه می دهد Event Loop به کار خود ادامه دهد.

Event Loop (صف رویداد): قلب تپنده جاوا اسکریپت ناهمگام

مدل اجرایی جاوا اسکریپت: Call Stack, Web APIs, Callback Queue, Microtask Queue

جاوا اسکریپت، با وجود تک رشته ای بودن، می تواند عملیات ناهمگام را مدیریت کند، و این جادو به لطف Event Loop (صف رویداد) و مکانیزم های مرتبط با آن رخ می دهد. برای درک چگونگی کارکرد جاوا اسکریپت ناهمگام، باید اجزای اصلی مدل اجرایی جاوا اسکریپت را بشناسیم:

  • Call Stack (پشته فراخوانی): این پشته، توابعی را که در حال حاضر در حال اجرا هستند، ردیابی می کند. هر بار تابعی فراخوانی می شود، به بالای پشته اضافه می شود و پس از اتمام، از پشته خارج می گردد.
  • Web APIs (API های مرورگر): این ها قابلیت هایی هستند که مرورگر (یا Node.js) فراتر از هسته جاوا اسکریپت ارائه می دهد، مانند setTimeout، DOM events و fetch. عملیات ناهمگام اغلب به این API ها واگذار می شوند.
  • Callback Queue (صف Callback ها / Task Queue): وقتی یک عملیات ناهمگام در Web API به پایان می رسد (مثلاً setTimeout منقضی می شود یا fetch داده ای را دریافت می کند)، Callback مربوط به آن به این صف اضافه می شود.
  • Microtask Queue (صف Microtask ها): این صف برای Promise ها و MutationObserver ها استفاده می شود و اولویت بالاتری نسبت به Callback Queue دارد.

چگونگی مدیریت عملیات ناهمگام توسط Event Loop

Event Loop یک حلقه بی نهایت است که به طور مداوم بررسی می کند که آیا Call Stack خالی است یا خیر. اگر Call Stack خالی باشد (به این معنی که هیچ کد همگامی در حال اجرا نیست)، Event Loop به سراغ Microtask Queue می رود و تمام Microtask های موجود را یکی پس از دیگری به Call Stack منتقل کرده و اجرا می کند. پس از خالی شدن Microtask Queue، Event Loop به سراغ Callback Queue می رود و اولین Callback موجود را به Call Stack منتقل کرده و اجرا می کند. این چرخه تا زمانی که برنامه در حال اجرا است ادامه پیدا می کند.

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

تفاوت Callback Queue و Microtask Queue

تفاوت اصلی بین Callback Queue (یا Task Queue) و Microtask Queue در اولویت اجرا و نوع وظایفی است که هر یک مدیریت می کنند. Microtask Queue اولویت بالاتری دارد؛ یعنی پس از هر بار که Call Stack خالی می شود، Event Loop ابتدا Microtask Queue را به طور کامل خالی می کند و سپس به سراغ Callback Queue می رود. این بدان معناست که Promise ها (و async/await که بر پایه آن هاست) تقریباً بلافاصله پس از اتمام کد همگام اجرا می شوند، در حالی که setTimeout و DOM events ممکن است کمی بیشتر طول بکشند زیرا در Callback Queue قرار می گیرند.


console.log('شروع');

setTimeout(() => {
  console.log('setTimeout اجرا شد');
}, 0); // حتی با تاخیر 0 میلی ثانیه، به Callback Queue می رود

Promise.resolve().then(() => {
  console.log('Promise اجرا شد'); // به Microtask Queue می رود
});

console.log('پایان');

// خروجی به این ترتیب خواهد بود:
// شروع
// پایان
// Promise اجرا شد
// setTimeout اجرا شد

مقایسه جامع تکنیک های بارگذاری غیرهمزمان: Callback، Promise و Async/Await

در این سفر به دنیای برنامه نویسی ناهمگام جاوا اسکریپت، سه تکنیک اصلی را بررسی کردیم: Callbacks، Promises و Async/Await. هر یک از این ها مزایا و معایب خاص خود را دارند و انتخاب بین آن ها بستگی به سناریوی خاص و ترجیحات توسعه دهنده دارد. در جدول زیر، به مقایسه این سه رویکرد می پردازیم:

ویژگی Callback ها Promise ها Async/Await
خوانایی کد پایین (مخصوصاً در Callbacks تو در تو – Callback Hell) متوسط تا بالا (با زنجیره سازی بهبود می یابد) بالا (شبیه به کد همگام، بسیار خوانا)
پیچیدگی پیاده سازی ساده برای عملیات تکی، بسیار پیچیده برای عملیات متوالی متوسط (نیاز به درک مفاهیم Promise) پایین (با ساختار try...catch آشنا)
مدیریت خطا دشوار (خطاهای پراکنده، نیاز به مدیریت در هر Callback) متوسط (مدیریت متمرکز با .catch()) بالا (مدیریت متمرکز با try...catch)
زنجیره سازی عملیات دشوار (Callback Hell) آسان و ساختاریافته (Promise Chaining) بسیار آسان و خطی
پشتیبانی مرورگرها همه مرورگرها (از ابتدا) اکثر مرورگرهای مدرن (ES6+) اکثر مرورگرهای مدرن (ES2017+)
کاربرد در سناریوهای مختلف رویدادهای ساده (مانند addEventListener)، توابع زمان بندی عملیات شبکه (fetch)، عملیات فایل (Node.js) عملیات پیچیده شبکه، تعامل با پایگاه داده، منطق کسب وکار ناهمگام

راهنمای انتخاب: چه زمانی از کدام تکنیک استفاده کنیم؟

انتخاب تکنیک مناسب برای بارگذاری غیرهمزمان JavaScript، به نیازهای خاص پروژه و پیچیدگی عملیات بستگی دارد:

  • Callbacks: برای عملیات ناهمگام بسیار ساده و تک مرحله ای، مانند مدیریت رویدادهای DOM (addEventListener) یا توابع زمان بندی (setTimeout)، Callback ها همچنان کاربرد دارند. اما برای هر چیز پیچیده تر، به سراغ Promise ها یا Async/Await بروید.
  • Promises: زمانی که نیاز به انجام چندین عملیات ناهمگام به صورت متوالی دارید و می خواهید کد خواناتری داشته باشید، Promise ها گزینه ای عالی هستند. fetch API یک نمونه بارز از کاربرد Promise هاست. همچنین، برای ترکیب کننده های Promise مانند Promise.all() در سناریوهای بارگذاری موازی، Promise ها بهترین انتخاب هستند.
  • Async/Await: برای اکثریت قریب به اتفاق عملیات ناهمگام در پروژه های مدرن، Async/Await در جاوا اسکریپت بهترین و توصیه شده ترین رویکرد است. این روش نه تنها کد را بسیار خواناتر و قابل نگهداری تر می کند، بلکه مدیریت خطا در کد ناهمگام را نیز بسیار ساده تر می سازد. به خصوص برای منطق کسب وکار پیچیده یا هر تابعی که شامل بیش از یک عملیات await باشد، Async/Await انتخاب برتر است.

بهینه سازی و بهترین شیوه ها در برنامه نویسی ناهمگام

تسلط بر تکنیک های بارگذاری غیرهمزمان JavaScript تنها آغاز راه است. برای ساخت برنامه های کاربردی وب قدرتمند و کارآمد، باید با بهترین شیوه ها و تکنیک های بهینه سازی نیز آشنا باشیم. این نکات به ما کمک می کنند تا کدی پایدارتر، سریع تر و قابل نگهداری تر بنویسیم.

مدیریت خطای پیشرفته

همانطور که قبلاً اشاره شد، مدیریت صحیح خطا در کد ناهمگام حیاتی است. استفاده از try...catch در async/await، .catch() در Promise ها، و الگوی error-first callback در Callback ها، باید به یک عادت تبدیل شود. همچنین، لاگ کردن خطاها و ارائه پیام های معنی دار به کاربر، برای دیباگینگ و بهبود تجربه کاربری ضروری است. در سناریوهای پیچیده تر، ممکن است به الگوی Retry (تلاش مجدد) برای درخواست هایی که ممکن است به دلیل خطاهای موقتی شبکه ناموفق باشند، نیاز پیدا کنیم.

جلوگیری از رقابت داده ای (Race Conditions)

رقابت داده ای زمانی رخ می دهد که ترتیب اجرای عملیات ناهمگام، منجر به نتایج غیرمنتظره یا نادرست شود. به عنوان مثال، ارسال دو درخواست API به صورت موازی که هر دو یک منبع را تغییر می دهند. برای جلوگیری از این وضعیت، می توان از الگوهایی مانند Lock (قفل کردن) یا از ترکیب کننده های Promise مانند Promise.all() برای اطمینان از تکمیل تمام عملیات قبل از پردازش نتایج نهایی استفاده کرد.

Debouncing و Throttling

این دو تکنیک برای کنترل نرخ اجرای توابع در پاسخ به رویدادهای مکرر (مانند تغییر اندازه پنجره، تایپ در فیلد جستجو یا اسکرول کردن) بسیار مفید هستند:

  • Debouncing: تضمین می کند که یک تابع تنها پس از گذشت یک دوره زمانی مشخص از آخرین فراخوانی اش، اجرا می شود. این کار برای پیاده سازی جستجوی زنده مفید است؛ زیرا از ارسال بی رویه درخواست به سرور با هر حرف تایپ شده جلوگیری می کند.
  • Throttling: تضمین می کند که یک تابع در یک دوره زمانی مشخص، بیش از یک بار اجرا نمی شود. این کار برای رویدادهایی مانند اسکرول یا تغییر اندازه پنجره مفید است تا از اجرای بیش از حد Callback و کاهش عملکرد جلوگیری شود.

Lazy Loading (بارگذاری تنبل) و Preloading منابع

Lazy Loading به معنای بارگذاری منابع (مانند تصاویر، ویدئوها یا ماژول های جاوا اسکریپت) تنها زمانی است که کاربر به آن ها نیاز پیدا می کند (مثلاً با اسکرول کردن به سمت پایین صفحه). این کار باعث افزایش سرعت وب سایت با جاوا اسکریپت ناهمگام و بهبود زمان بارگذاری اولیه صفحه می شود. Preloading (بارگذاری قبلی) برعکس Lazy Loading است؛ در اینجا منابعی که احتمالاً در آینده نزدیک مورد نیاز خواهند بود، در پس زمینه بارگذاری می شوند تا تجربه کاربری روان تر شود.

Cancellation Tokens

در برخی موارد، ممکن است نیاز به لغو درخواست های ناهمگام در حال انجام داشته باشیم. به عنوان مثال، اگر کاربر قبل از اتمام یک درخواست API، صفحه را ترک کند یا یک درخواست جدید ارسال کند. Cancellation Tokens (یا AbortController در Fetch API) مکانیزمی را برای لغو درخواست ها قبل از اتمام آن ها فراهم می کنند، که به بهینه سازی عملکرد جاوا اسکریپت و جلوگیری از هدر رفتن منابع کمک می کند.

استفاده از Web Workers

برای عملیات سنگین و محاسباتی که CPU-bound هستند و ممکن است حتی با تکنیک های بارگذاری غیرهمزمان JavaScript نیز رشته اصلی را تحت تأثیر قرار دهند، می توان از Web Workers استفاده کرد. Web Workers به جاوا اسکریپت اجازه می دهند تا اسکریپت ها را در رشته های پس زمینه اجرا کند، مستقل از رشته اصلی مرورگر. این کار تضمین می کند که UI حتی در حین انجام محاسبات فشرده، کاملاً پاسخگو باقی بماند.

استفاده هوشمندانه از Web Workers می تواند بار پردازشی سنگین را از رشته اصلی جاوا اسکریپت جدا کرده و بهینه سازی قابل توجهی در عملکرد برنامه های وب ایجاد کند.

سناریوهای عملی: تکنیک های غیرهمزمان در دنیای واقعی

استفاده از تکنیک های بارگذاری غیرهمزمان JavaScript تنها یک مفهوم تئوری نیست؛ بلکه یک ابزار حیاتی در ساخت برنامه های کاربردی وب مدرن و تعاملی است. در اینجا به چند سناریوی عملی اشاره می کنیم که در آن ها، برنامه نویسی ناهمگام نقش کلیدی ایفا می کند:

  • فراخوانی API برای نمایش داده های پویا: زمانی که می خواهید لیستی از محصولات، پروفایل های کاربر یا هر نوع داده دیگری را از یک سرور دریافت کرده و در رابط کاربری نمایش دهید، از fetch API همراه با Promise ها یا async/await استفاده می کنید.
  • بارگذاری همزمان چندین تصویر یا فایل: در وب سایت های گالری تصاویر یا پلتفرم های آپلود فایل، Promise.all() به شما امکان می دهد تا چندین منبع را به صورت موازی بارگذاری کرده و پس از اتمام همه آن ها، عملیات بعدی را آغاز کنید.
  • ارسال فرم های ناهمگام: به جای بارگذاری مجدد کل صفحه پس از ارسال یک فرم، می توانید از تکنیک های ناهمگام برای ارسال داده های فرم به سرور استفاده کنید و سپس پاسخ را بدون رفرش صفحه پردازش و رابط کاربری را به روزرسانی کنید.
  • پیاده سازی جستجوی زنده (Live Search): با استفاده از Debouncing و فراخوانی های ناهمگام API، می توانید نتایج جستجو را به محض تایپ کاربر، بدون تأخیر و بدون نیاز به کلیک بر دکمه جستجو، نمایش دهید.
  • به روزرسانی لحظه ای UI (با استفاده از WebSockets): برای برنامه هایی مانند چت آنلاین، اعلان ها یا بازی های چندنفره، WebSockets ارتباطی دوطرفه و پایدار با سرور فراهم می کنند که امکان دریافت به روزرسانی های لحظه ای را به صورت ناهمگام فراهم می سازد.

نتیجه گیری: تسلط بر ناهمگامی، کلید توسعه وب مدرن

در این مقاله جامع، سفری را از مفاهیم اولیه جاوا اسکریپت همگام آغاز کردیم و به عمق تکنیک های بارگذاری غیرهمزمان JavaScript، شامل Callbacks، Promises و Async/Await در جاوا اسکریپت، پرداختیم. دریافتیم که چگونه این تکنیک ها، چالش های ذاتی تک رشته ای بودن جاوا اسکریپت را حل می کنند و وب سایت ها و برنامه های کاربردی را به سمت پاسخگویی سریع تر و تجربه کاربری بهتر سوق می دهند.

از پیچیدگی های Callback Hell تا سادگی و خوانایی Async/Await، هر قدم در این مسیر، ابزارهای قدرتمندتری را برای مدیریت عملیات ناهمگام در اختیار ما قرار داده است. درک دقیق Event Loop نیز به ما کمک کرد تا مکانیزم پنهان پشت این جادو را کشف کنیم و بهینه سازی های پیشرفته ای مانند مدیریت خطا در کد ناهمگام، Debouncing و Throttling و Lazy Loading را فرا گرفتیم. تسلط بر این مفاهیم و به کارگیری صحیح آن ها، نه تنها به افزایش سرعت وب سایت با جاوا اسکریپت ناهمگام کمک می کند، بلکه راه را برای خلق تجربه های کاربری بی نظیر و توسعه وب مدرن هموار می سازد.

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

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