JavaScript یکی از پرکاربردترین زبانهای برنامهنویسی وب است که به توسعهدهندگان اجازه میدهد وبسایتها و اپلیکیشنهای تعاملی بسازند. اگر تازه با این زبان آشنا شدهاید یا در حال ارتقای دانش خود هستید، این آموزش JavaScript به شما کمک خواهد کرد تا عملکردهای پیشرفته در JavaScript را به طور کامل و با جزئیات یاد بگیرید. از جمله مهمترین مباحث این آموزش، مدیریت عملیات غیرهمزمان است که به شما کمک میکند برنامههای بهینهتری بنویسید.
توسعهدهی با JavaScript Asynchronous
در JavaScript، مدیریت عملیات همزمان و غیرهمزمان برای بهینهسازی تجربه کاربری و کارآمدتر کردن عملکرد برنامهها بسیار اهمیت دارد. درک تفاوت میان این دو نوع عملیات و کاربرد هر کدام، از جمله مهارتهای اساسی برای هر توسعهدهنده جاوااسکریپت است.
عملیات همزمان (Synchronous) در JavaScript
عملیات همزمان (Synchronous) در جاوااسکریپت به گونهای کار میکند که هر دستور یا عملیات تا زمانی که به پایان نرسد، دستور یا عملیات بعدی اجرا نمیشود. به این معنی که کد خط به خط از بالا به پایین اجرا میشود و زمانی که یک عملیات در حال انجام است، سایر کدها باید منتظر بمانند تا آن عملیات به پایان برسد. این مدل اجرای کد ممکن است در برخی موارد مشکلساز شود، به ویژه زمانی که عملیات سنگینی مانند درخواست از سرور یا انجام محاسبات پیچیده انجام میشود. زیرا این عملیاتها میتوانند باعث متوقف شدن کل برنامه و تأخیر در پاسخگویی شوند.
عملیات غیرهمزمان (Asynchronous) در JavaScript
جاوااسکریپت به دلیل تکنخی (single-threaded) بودن، تنها میتواند یک کار را در یک لحظه انجام دهد. با این حال، از طریق عملیات غیرهمزمان (Asynchronous) قادر است چندین وظیفه را به صورت موازی مدیریت کند بدون اینکه دیگر وظایف متوقف شوند. در این مدل، هنگامی که یک عملیات غیرهمزمان آغاز میشود، جاوااسکریپت منتظر پایان یافتن آن نمیماند و بلافاصله به سراغ دستورات بعدی میرود.
جاوااسکریپت برای اجرای عملیات غیرهمزمان از یک موتور داخلی به نام Event Loop استفاده میکند که در کنار Callback Queue و Call Stack، کمک میکند تا عملیات غیرهمزمان به درستی مدیریت شوند.
Event Loop: مکانیسمی است که وظایف موجود در Callback Queue را یکییکی به Call Stack منتقل میکند. به این ترتیب، پس از اتمام اجرای دستورات همزمان، Event Loop چک میکند که آیا وظیفهای در Callback Queue وجود دارد یا خیر.
Callback Queue: صفی از وظایف غیرهمزمان است که پس از اتمام اجرای همزمان، به Call Stack منتقل میشوند.
Call Stack: جایی که توابع و عملیات جاری در آن اجرا میشوند. زمانی که عملیات در Call Stack کامل شد، به سراغ عملیات بعدی میرود.
به طور کلی، عملیات غیرهمزمان زمانی به کار میرود که میخواهیم فعالیتی را بدون توقف سایر کدها انجام دهیم. این نوع برنامهنویسی برای مواردی مانند ارسال درخواستهای HTTP به سرور، خواندن و نوشتن فایلها، و ارتباط با پایگاهدادهها استفاده میشود.
نمونهای از عملیات همزمان و غیرهمزمان
یک مثال ساده برای نشان دادن تفاوت بین همزمان و غیرهمزمان میتواند استفاده از console.log به همراه setTimeout باشد:
console.log("مرحله ۱");
setTimeout(() => {
console.log("مرحله ۲ - عملیات غیرهمزمان");
}, 2000);
console.log("مرحله ۳");
خروجی این کد به صورت زیر خواهد بود:
مرحله ۱ مرحله ۳ مرحله ۲ - عملیات غیرهمزمان
در اینجا، setTimeout یک عملیات غیرهمزمان است که برای ۲ ثانیه به تعویق میافتد. در همین حین، جاوااسکریپت مرحله ۳ را بلافاصله اجرا میکند و پس از گذشت ۲ ثانیه، مرحله ۲ را اجرا میکند. این رفتار نشاندهندهی خاصیت غیرهمزمان بودن جاوااسکریپت است.
مزایا و معایب عملیات غیرهمزمان
مزایا
بهبود کارایی برنامهها: برنامهها میتوانند چندین وظیفه را به صورت همزمان مدیریت کنند.
افزایش پاسخگویی: کاربر تجربه روانتری از کار با برنامه خواهد داشت زیرا عملیات سنگین دیگر باعث توقف برنامه نمیشوند.
معایب
پیچیدگی بیشتر: مدیریت و نوشتن کدهای غیرهمزمان میتواند چالشبرانگیز باشد، به خصوص زمانی که از callbackها یا Promises استفاده میکنیم.
احتمال بروز مشکلات همگامسازی: اجرای چندین عملیات به صورت همزمان میتواند منجر به بروز خطاهایی در همگامسازی دادهها شود.
با استفاده از روشهای مختلف مانند Callback Functions، Promises و Async/Await میتوان این پیچیدگی را کاهش داد و کدهای غیرهمزمان را به راحتی مدیریت کرد.
Callback Functions
در JavaScript، Callback Function یکی از اصلیترین و ابتداییترین روشها برای مدیریت عملیات غیرهمزمان است. Callback به معنای “فراخوانی مجدد” است و به طور ساده به تابعی گفته میشود که به عنوان آرگومان به تابع دیگری ارسال میشود و در زمان مناسب فراخوانی میشود. این روش به ما کمک میکند تا بتوانیم وظایفی که نیاز به زمانبندی یا انتظار دارند، به راحتی مدیریت کنیم، بدون اینکه اجرای دیگر بخشهای کد را متوقف کنیم.
مفهوم Callback در JavaScript
در عملیاتهای غیرهمزمان، اغلب نیاز داریم پس از اتمام یک وظیفه، وظیفه دیگری انجام شود. به عنوان مثال، ممکن است بخواهیم پس از دریافت دادهها از یک سرور، آنها را نمایش دهیم. در این شرایط، میتوانیم از یک تابع Callback استفاده کنیم. این تابع به عنوان آرگومان به تابع اصلی ارسال میشود و زمانی که عملیات اصلی به پایان رسید، فراخوانی میشود.
Callback Functions هم در عملیاتهای همزمان و هم در عملیاتهای غیرهمزمان استفاده میشوند، اما بیشترین کاربرد آنها در مدیریت عملیاتهای غیرهمزمان است. در این حالت، تابع اصلی کاری را انجام میدهد، سپس تابع Callback را فراخوانی میکند تا ادامه فرآیند اجرا شود.
نمونهای از Callback Functions
در مثال زیر، تابع greet نام یک شخص را به عنوان پارامتر دریافت کرده و یک پیام خوشآمدگویی را به همراه نام نمایش میدهد. همچنین یک تابع دیگر (تابع sayGoodbye) به عنوان Callback ارسال شده است تا پس از اجرای خوشآمدگویی، یک پیام خداحافظی را نمایش دهد.
function greet(name, callback) {
console.log('Hello, ' + name + '!');
callback();
}
function sayGoodbye() {
console.log('Goodbye!');
}
greet('Ali', sayGoodbye);
خروجی این کد به این صورت خواهد بود:
Hello, Ali! Goodbye!
در اینجا، تابع sayGoodbye به عنوان یک Callback به تابع greet ارسال شده و زمانی که عملیات console.log(‘Hello, ‘ + name + ‘!’); در greet به اتمام رسید، تابع sayGoodbye اجرا میشود.
کاربرد Callback در عملیات غیرهمزمان
یکی از کاربردهای رایج Callback در جاوااسکریپت، زمانی است که میخواهیم دادهای را از یک سرور دریافت کنیم یا درخواستی HTTP را مدیریت کنیم. به عنوان مثال، در متد setTimeout میتوانیم از Callback استفاده کنیم تا یک عملیات خاص پس از مدت زمان مشخصی اجرا شود.
مثال استفاده از setTimeout:
function fetchData(callback) {
console.log("Requesting data...");
setTimeout(() => {
console.log("Data received.");
callback();
}, 3000);
}
function processData() {
console.log("Processing data...");
}
fetchData(processData);
خروجی این کد به این شکل است:
Requesting data... Data received. Processing data...
در اینجا، fetchData درخواست دریافت داده را آغاز میکند. پس از گذشت ۳ ثانیه، پیام “Data received.” نمایش داده میشود و سپس تابع processData که به عنوان Callback ارسال شده، اجرا میشود.
چالشهای Callback Functions – Callback Hell
استفاده مکرر از Callbackها به ویژه زمانی که چندین عملیات غیرهمزمان باید به ترتیب اجرا شوند، میتواند به ساختار پیچیده و دشوار منجر شود که اصطلاحاً به آن Callback Hell گفته میشود. در این حالت، کد ما به شکل “پلهای” و تو در تو نوشته میشود و خوانایی آن بسیار کاهش مییابد.
به عنوان مثال:
doTask1(function(result1) {
doTask2(result1, function(result2) {
doTask3(result2, function(result3) {
doTask4(result3, function(result4) {
console.log("All tasks completed");
});
});
});
});
این نوع ساختار باعث میشود کد بسیار پیچیده و خواندن و اشکالزدایی آن مشکل شود. برای جلوگیری از چنین وضعیتی، روشهای دیگری مانند Promises و async/await در جاوااسکریپت معرفی شدهاند که به ما کمک میکنند کد غیرهمزمان خواناتری بنویسیم.
مزایای استفاده از Callback Functions
مدیریت عملیات غیرهمزمان: Callbackها به ما امکان میدهند عملیاتهای غیرهمزمان را بدون توقف سایر بخشهای کد مدیریت کنیم.
ساده و قابل فهم برای کاربردهای ساده: در بسیاری از موارد ساده، استفاده از Callbackها آسان و قابل فهم است.
معایب استفاده از Callback Functions
Callback Hell: همانطور که توضیح داده شد، استفاده از توابع تو در تو میتواند منجر به کدی شود که خوانایی و مدیریت آن دشوار است.
اشکالزدایی دشوار: در کدهایی با تعداد زیادی Callback، یافتن خطا و اشکالزدایی مشکل میشود.
حفظ حالت و سلسلهمراتب پیچیده: در برنامههای پیچیده، استفاده از Callbackها ممکن است باعث از دست دادن کنترل جریان برنامه و سردرگمی شود.
با این حال، Callback Functions یکی از مفاهیم بنیادی در جاوااسکریپت هستند و درک آنها به توسعهدهندگان کمک میکند تا بتوانند عملیاتهای غیرهمزمان را بهتر مدیریت کنند. برای حل مشکلات Callbackها و جلوگیری از پیچیدگی زیاد، Promises و async/await به عنوان جایگزینهای مدرن و بهینهتر معرفی شدهاند که کدهای غیرهمزمان را خواناتر و مدیریت آنها را آسانتر میکنند.
Promises و نحوه استفاده از آنها
Promises یکی از مفاهیم مهم در جاوااسکریپت برای مدیریت عملیاتهای غیرهمزمان است و به عنوان جایگزینی برای Callback functions معرفی شده است. Promises به ما این امکان را میدهند که نتایج عملیاتهای غیرهمزمان را به شکل خواناتر و قابل مدیریتتری کنترل کنیم. در واقع، با استفاده از Promises میتوانیم کدهای خود را از پیچیدگیهای Callback Hell خلاص کنیم و از ساختار سادهتر و مرتبتری بهره ببریم.
Promise چیست؟
یک Promise یک شیء است که نمایندهی نتیجهی یک عملیات غیرهمزمان است و میتواند در سه حالت زیر قرار داشته باشد:
Pending (منتظر): عملیات هنوز در حال اجرا است و نتیجهای ندارد.
Fulfilled (موفق): عملیات به پایان رسیده و با موفقیت انجام شده است.
Rejected (رد شده): عملیات به پایان رسیده اما با شکست مواجه شده است.
Promise در ابتدا در حالت Pending قرار دارد و سپس با انجام عملیات، به یکی از حالتهای Fulfilled یا Rejected تبدیل میشود. هنگامی که یک Promise به یکی از این دو حالت نهایی رسید، وضعیت آن ثابت میشود و دیگر تغییر نمیکند.
ساختار Promise
یک Promise از دو تابع اصلی استفاده میکند: resolve و reject.
resolve: هنگامی که عملیات با موفقیت به پایان میرسد، فراخوانی میشود و نتیجه موفقیتآمیز را برمیگرداند.
reject: هنگامی که عملیات با شکست مواجه میشود، فراخوانی شده و علت شکست را برمیگرداند.
مثال ساده از یک Promise
در مثال زیر، یک Promise ساخته شده که یک عملیات ساده را شبیهسازی میکند. در این مثال، اگر مقدار success برابر با true باشد، Promise با موفقیت انجام میشود و پیام موفقیت برمیگردد؛ در غیر این صورت، عملیات شکست میخورد و پیام خطا برگردانده میشود.
let promise = new Promise((resolve, reject) => {
let success = true; // میتوانید این را به false تغییر دهید تا نتیجه رد شده را ببینید
if (success) {
resolve("Operation successful!");
} else {
reject("Operation failed!");
}
});
promise
.then(result => console.log(result)) // در صورت موفقیت نتیجه را نمایش میدهد
.catch(error => console.log(error)); // در صورت شکست پیام خطا را نمایش میدهد
در اینجا:
اگر عملیات موفق باشد، پیغام “Operation successful!” نمایش داده میشود.
اگر عملیات شکست بخورد، پیغام “Operation failed!” نمایش داده میشود.
متدهای then و catch در Promise
برای کار با Promiseها از متدهای زیر استفاده میشود:
then: این متد به Promise اضافه میشود تا نتیجه موفقیتآمیز (resolve) را دریافت و پردازش کند.
catch: این متد زمانی اجرا میشود که Promise به دلیل خطا یا مشکل در وضعیت rejected قرار گیرد و خطا را مدیریت کند.
استفاده از then و catch باعث میشود کد خواناتر و واضحتر باشد و مدیریت خطا به صورت سادهتری انجام شود.
مثال عملی از Promise در درخواست HTTP
فرض کنید میخواهیم دادههایی را از یک سرور دریافت کنیم. در اینجا از fetch که یک Promise برمیگرداند استفاده میکنیم. با استفاده از then و catch میتوانیم به سادگی درخواست را مدیریت کنیم.
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then(data => console.log("Data received:", data))
.catch(error => console.log("Error fetching data:", error));
در این مثال:
اگر درخواست به درستی انجام شود، دادهها به شکل JSON دریافت شده و نمایش داده میشوند.
اگر درخواست با خطا مواجه شود (مثل عدم دسترسی به سرور)، خطا در catch مدیریت و پیام مناسب نمایش داده میشود.
Promise Chaining (زنجیرهسازی Promiseها)
یکی از ویژگیهای مهم Promiseها، امکان زنجیرهسازی آنها با استفاده از متد then است. به این ترتیب، میتوان عملیاتهای متوالی را به شکل خوانا و بدون استفاده از تو در تو نویسی (Callback Hell) انجام داد.
مثال:
new Promise((resolve, reject) => {
resolve(10);
})
.then(result => {
console.log(result); // 10
return result * 2;
})
.then(result => {
console.log(result); // 20
return result * 3;
})
.then(result => {
console.log(result); // 60
})
.catch(error => console.log(error));
در این مثال، هر then نتیجهای را برمیگرداند که به عنوان ورودی به then بعدی منتقل میشود. اگر هر کدام از این مراحل خطایی داشته باشد، catch اجرا میشود.
مزایای استفاده از Promiseها
ساختار خواناتر: کدهای غیرهمزمان به مراتب خواناتر و قابل فهمتر میشوند.
مدیریت بهتر خطاها: با استفاده از catch، خطاهای مختلف به راحتی مدیریت میشوند.
جلوگیری از Callback Hell: Promises به ما این امکان را میدهند که کدهای تو در تو را کاهش داده و ساختاری زنجیرهای برای عملیاتهای غیرهمزمان ایجاد کنیم.
معایب استفاده از Promiseها
پیچیدگی در کدهای پیچیده: در مواردی که چندین Promise باید همزمان اجرا شوند و نتیجهی همه آنها بررسی شود، مدیریت Promises میتواند کمی پیچیده باشد.
آشنایی کمتر در ابتدای یادگیری: برای کسانی که تازه شروع به یادگیری جاوااسکریپت میکنند، درک Promises ممکن است کمی دشوار باشد.
استفاده از Promise.all و Promise.race
Promise.all: این متد مجموعهای از Promises را به صورت همزمان اجرا میکند و زمانی که همه آنها به وضعیت fulfilled رسیدند، نتیجه آنها را برمیگرداند. اگر یکی از Promiseها رد شود، کل مجموعه رد میشود.
مثال:
let promise1 = Promise.resolve(3); let promise2 = Promise.resolve(5); let promise3 = Promise.resolve(7); Promise.all([promise1, promise2, promise3]) .then(results => console.log(results)) // [3, 5, 7] .catch(error => console.log(error));
Promise.race: این متد نیز مجموعهای از Promises را به صورت همزمان اجرا میکند، اما به محض اینکه یکی از Promiseها به نتیجه (fulfilled یا rejected) رسید، همان نتیجه را برمیگرداند.
مثال:
let promiseA = new Promise(resolve => setTimeout(resolve, 100, 'A')); let promiseB = new Promise(resolve => setTimeout(resolve, 200, 'B')); Promise.race([promiseA, promiseB]) .then(result => console.log(result)) // A .catch(error => console.log(error));
Promises یک ابزار قدرتمند و مؤثر برای مدیریت عملیاتهای غیرهمزمان در جاوااسکریپت هستند که خوانایی و مدیریت کد را بهبود میبخشند. به جای استفاده از Callbackهای تو در تو، Promises امکان زنجیرهسازی و مدیریت بهتر خطاها را فراهم میکنند. Promises همچنین راهکارهای پیشرفتهای مانند Promise.all و Promise.race را ارائه میدهند که کار با عملیاتهای غیرهمزمان را در پروژههای پیچیده آسانتر میکنند.
async/await
async/await یک روش مدرن و پیشرفته برای مدیریت Promises در جاوااسکریپت است که کد غیرهمزمان را ساده و خوانا میکند، به طوری که کد غیرهمزمان شبیه به کد همزمان نوشته میشود. این ویژگیها باعث میشود توسعهدهندگان بتوانند عملیاتهای پیچیده غیرهمزمان را به شکل قابل فهمتر و مختصرتر بنویسند و مدیریت کنند.
مفهوم async و await
در این روش:
کلمه کلیدی async قبل از تعریف یک تابع قرار میگیرد و این تابع را به یک تابع غیرهمزمان تبدیل میکند که همیشه یک Promise برمیگرداند.
از await برای منتظر ماندن نتایج یک Promise استفاده میشود. با استفاده از await، اجرای کد به صورت موقتی متوقف میشود تا زمانی که Promise مربوطه انجام شود (خواه موفقیتآمیز باشد یا رد شود). این ساختار، پیچیدگی استفاده از then و catch در زنجیرههای طولانی Promises را کاهش میدهد و باعث خوانایی بیشتر کد میشود.
چگونه از async/await استفاده کنیم؟
برای استفاده از async/await مراحل زیر را باید رعایت کنیم:
تعریف تابع به صورت async: برای اینکه بتوانید از await استفاده کنید، ابتدا باید تابع را به کمک کلمه کلیدی async به یک تابع غیرهمزمان تبدیل کنید.
استفاده از await برای دریافت نتیجه Promise: با استفاده از await، میتوانید منتظر بمانید تا یک Promise به اتمام برسد و سپس نتیجه را در یک متغیر ذخیره کنید.
مثال از async/await در جاوااسکریپت
مثال زیر نحوه استفاده از async/await را برای دریافت دادهها از یک API و پردازش آنها نشان میدهد:
async function fetchData() {
try {
let response = await fetch('https://jsonplaceholder.typicode.com/posts');
let data = await response.json();
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchData();
در این مثال:
تابع fetchData با کلمه کلیدی async تعریف شده است و یک Promise را برمیگرداند.
داخل این تابع، await جلوی فراخوانی fetch قرار گرفته است، بنابراین جاوااسکریپت منتظر میماند تا درخواست HTTP تکمیل شود.
اگر درخواست موفقیتآمیز باشد، دادهها دریافت و به صورت JSON تبدیل میشوند. اگر خطایی رخ دهد، در بخش catch مدیریت میشود.
مزایای استفاده از async/await
خوانایی بالا: کد غیرهمزمانی که با async/await نوشته شده، شبیه به کد همزمان به نظر میرسد و خواندن آن برای توسعهدهندگان سادهتر است.
کاهش پیچیدگی زنجیرههای Promise: استفاده از then و catch در زنجیرههای طولانی میتواند پیچیده و گیجکننده باشد. با async/await، این زنجیرهها به حداقل رسیده و کد کوتاهتر میشود.
مدیریت بهتر خطاها: با استفاده از try…catch میتوان خطاهای احتمالی در کد را به سادگی مدیریت کرد و به این ترتیب کنترل بیشتری روی خطاهای غیرهمزمان داشت.
ترکیب async/await و چندین Promise
هنگام کار با چندین Promise، میتوان async/await را با متدهای Promise.all و Promise.race ترکیب کرد تا کارایی کد بهبود یابد. این روش به ما کمک میکند تا چندین درخواست غیرهمزمان را به طور همزمان ارسال کنیم و به نتیجهی همه آنها منتظر بمانیم.
مثال:
async function fetchData() {
try {
let [posts, users] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json()),
fetch('https://jsonplaceholder.typicode.com/users').then(res => res.json())
]);
console.log("Posts:", posts);
console.log("Users:", users);
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchData();
در این مثال:
از Promise.all برای همزمانی درخواستها استفاده شده است.
await منتظر میماند تا هر دو درخواست (دریافت posts و users) انجام شود.
اگر هر یک از درخواستها شکست بخورد، خطا در بخش catch مدیریت میشود.
نکات مهم درباره async/await
await تنها در توابع async: استفاده از await تنها در داخل توابعی که با async تعریف شدهاند امکانپذیر است. در خارج از این توابع، استفاده از await منجر به خطا خواهد شد.
مسدودسازی درون تابع: در حالی که await باعث توقف درون تابع میشود، کل برنامه متوقف نمیشود؛ بنابراین این توقف تنها در سطح کد داخلی تابع async رخ میدهد و سایر بخشهای کد به اجرای خود ادامه میدهند.
چالشهای async/await
عدم پشتیبانی در حلقههای forEach: به دلیل ساختار forEach، نمیتوان از await درون آن استفاده کرد. برای اجرای عملیات غیرهمزمان در حلقه، بهتر است از حلقههای for…of استفاده کنید.
مثال:
async function processItems(items) {
for (let item of items) {
await processItem(item);
}
}
ترکیب نادرست با عملیاتهای همزمان: هرچند async/await کد را خوانا میکند، اما استفاده نادرست از آن میتواند باعث کاهش کارایی شود. برای مثال، اگر نیاز به اجرای همزمان چند عملیات داریم، بهتر است از Promise.all استفاده کنیم و از انجام پشت سر هم عملیات اجتناب کنیم.
async/await یک ابزار قدرتمند برای مدیریت کدهای غیرهمزمان و عملیاتهای وابسته به Promises در جاوااسکریپت است که باعث میشود کد بسیار خواناتر و نگهداری آن آسانتر باشد. با یادگیری اصول استفاده از async/await و ترکیب آن با روشهایی مانند Promise.all، میتوانید عملیاتهای پیچیده و غیرهمزمان را به بهترین شکل مدیریت کنید و از مشکلاتی مانند Callback Hell اجتناب کنید.
بکارگیری setTimeout و setInterval
در جاوااسکریپت، setTimeout و setInterval دو متد مهم برای مدیریت زمانبندی در عملیاتهای غیرهمزمان هستند که به شما اجازه میدهند وظایف و کدها را با تاخیر یا بهصورت تکراری اجرا کنید. این دو متد به خصوص در ساختارهای تعاملی و انجام وظایف پسزمینه مانند بارگذاری دادهها، مدیریت انیمیشنها و بهروزرسانیهای خودکار بسیار پرکاربرد هستند.
setTimeout چیست؟
setTimeout یک متد برای به تعویق انداختن اجرای یک تابع تا مدت زمان مشخصی است. با استفاده از این متد، میتوانید یک کد یا تابع را پس از گذشت یک بازه زمانی خاص (به میلیثانیه) اجرا کنید. setTimeout برای یک بار اجرا میشود و پس از تکمیل اجرا، وظیفه خود را پایان میدهد.
ساختار setTimeout:
setTimeout(function, delay, arg1, arg2, ...);
function: تابعی که پس از پایان تاخیر اجرا میشود.
delay: مقدار تاخیر به میلیثانیه (۱۰۰۰ میلیثانیه برابر با ۱ ثانیه است).
arg1, arg2, …: پارامترهایی که میتوانید به تابع ارسال کنید.
مثال ساده از استفاده setTimeout:
setTimeout(() => {
console.log("This message will display after 2 seconds");
}, 2000);
در این مثال، پیغام مورد نظر پس از ۲ ثانیه (۲۰۰۰ میلیثانیه) نمایش داده میشود.
مثالهای پیشرفتهتر از setTimeout
استفاده از setTimeout برای فراخوانی تابع با پارامتر:
function greet(name) {
console.log("Hello, " + name + "!");
}
setTimeout(greet, 3000, "Ali");
در اینجا، greet با پارامتر “Ali” پس از ۳ ثانیه اجرا میشود.
ایجاد تاخیر در یک حلقه با setTimeout:
با استفاده از setTimeout، میتوانید اجرای عملیات را در یک حلقه با تاخیر انجام دهید.
for (let i = 1; i <= 5; i++) {
setTimeout(() => {
console.log("Message number " + i);
}, i * 1000);
}
این کد، هر یک ثانیه یک پیام جدید را نمایش میدهد و از ۱ تا ۵ ادامه مییابد.
setInterval چیست؟
setInterval برای اجرای تکراری یک تابع در بازههای زمانی مشخص استفاده میشود. برخلاف setTimeout که تنها یک بار اجرا میشود، setInterval تا زمانی که صراحتاً متوقف نشود، تابع مورد نظر را بارها و بارها اجرا میکند.
ساختار setInterval:
setInterval(function, interval, arg1, arg2, ...);
function: تابعی که در بازههای زمانی مشخص شده، تکرار خواهد شد.
interval: مدت زمان بین هر تکرار (به میلیثانیه).
arg1, arg2, …: پارامترهایی که به تابع ارسال میشوند.
مثال ساده از setInterval:
setInterval(() => {
console.log("This message will display every 3 seconds");
}, 3000);
در این مثال، پیغام هر ۳ ثانیه یک بار نمایش داده میشود تا زمانی که setInterval متوقف شود.
مثالهای پیشرفتهتر از setInterval
ایجاد یک تایمر ساده با setInterval:
let counter = 0;
const timer = setInterval(() => {
counter += 1;
console.log("Timer:", counter);
if (counter === 10) {
clearInterval(timer);
}
}, 1000);
در این مثال، یک تایمر ایجاد شده که هر ثانیه یک بار شمارنده را افزایش میدهد. زمانی که شمارنده به ۱۰ برسد، clearInterval اجرا میشود و setInterval متوقف میگردد.
استفاده از setInterval برای ساخت یک ساعت ساده:
const clock = setInterval(() => {
let now = new Date();
console.log(now.toLocaleTimeString());
}, 1000);
این کد هر ثانیه زمان فعلی را به صورت ساعت محلی نمایش میدهد.
متوقف کردن setTimeout و setInterval
برای متوقف کردن setTimeout و setInterval، از متدهای clearTimeout و clearInterval استفاده میشود.
clearTimeout: این متد برای جلوگیری از اجرای setTimeout قبل از انقضای مدت زمان مشخص شده استفاده میشود.
clearInterval: این متد برای متوقف کردن setInterval استفاده میشود و جلوی اجرای مکرر را میگیرد.
مثال:
let timeoutId = setTimeout(() => {
console.log("This will not be displayed");
}, 2000);
// متوقف کردن `setTimeout`
clearTimeout(timeoutId);
let intervalId = setInterval(() => {
console.log("This message will display every 3 seconds");
}, 3000);
// متوقف کردن `setInterval` پس از 10 ثانیه
setTimeout(() => {
clearInterval(intervalId);
console.log("Interval stopped");
}, 10000);
موارد استفاده و کاربردها
ایجاد تاخیر در نمایش پیام یا تغییرات در UI: با setTimeout میتوانید تغییرات خاصی را در UI پس از مدت زمان مشخصی انجام دهید.
بهروزرسانیهای خودکار و دریافت دادهها: با setInterval میتوانید درخواستهای مکرر به سرور بفرستید تا دادههای جدید را دریافت کنید.
انیمیشنها: برای ایجاد انیمیشنهای ساده، میتوان از setInterval استفاده کرد.
نکات مهم درباره setTimeout و setInterval
دقت پایین در زمانبندی: در صورت اجرای عملیاتهای سنگین دیگر در مرورگر، setTimeout و setInterval ممکن است کمی تأخیر داشته باشند.
مسدودسازی اجرای کد: هرچند این متدها غیرهمزمان هستند، اما استفاده مکرر از setInterval میتواند مرورگر را به دلیل اشغال پردازنده دچار مشکل کند.
حل مشکل نشت حافظه (Memory Leak): هنگام استفاده از setInterval، اگر متد clearInterval فراخوانی نشود، تابع مربوطه به صورت دائمی در حافظه باقی خواهد ماند و منجر به نشت حافظه میشود.
استفاده از setTimeout و setInterval در async/await
بهطور پیشفرض، setTimeout و setInterval با async/await سازگار نیستند، اما میتوانید با استفاده از Promise، آنها را در کدهای async/await ترکیب کنید.
مثال:
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function example() {
console.log("Waiting for 2 seconds...");
await delay(2000);
console.log("Done!");
}
example();
در اینجا، delay به عنوان یک Promise با setTimeout ساخته شده و میتوان از آن در async/await استفاده کرد.
setTimeout و setInterval ابزارهای پایهای برای مدیریت زمانبندی در جاوااسکریپت هستند و به خصوص در برنامهنویسی سمت کلاینت برای مدیریت انیمیشنها، درخواستهای HTTP مکرر و بهروزرسانیهای خودکار بسیار کاربردی هستند. با این حال، باید دقت کرد که استفاده بیش از حد از آنها میتواند بر کارایی برنامه تأثیر منفی بگذارد.
عملکرد غیرهمزمان با Fetch API
Fetch API یکی از قابلیتهای جدید و پرکاربرد جاوااسکریپت است که برای ارسال درخواستهای HTTP و دریافت پاسخ به کار میرود. با این API میتوان دادهها را از سرور دریافت یا به سرور ارسال کرد و نتایج را با استفاده از ساختار غیرهمزمان Promises مدیریت کرد. Fetch API به دلیل سادگی در استفاده و پشتیبانی از Promises، جایگزین مناسبی برای XMLHttpRequest قدیمی است و در کدنویسی HTTP تجربه بهتری فراهم میکند.
نحوه کار Fetch API
با استفاده از متد fetch میتوان درخواست HTTP را به یک URL ارسال کرد و دادهها را به صورت غیرهمزمان دریافت نمود. این متد به طور پیشفرض یک Promise برمیگرداند و بنابراین میتوان از then و catch برای مدیریت نتیجه و خطاهای احتمالی استفاده کرد.
ساختار کلی یک درخواست fetch به صورت زیر است:
fetch(url, options)
.then(response => {
// پردازش پاسخ
})
.catch(error => {
// مدیریت خطا
});
url: آدرس URL مورد نظر برای ارسال درخواست.
options: پارامتر اختیاری برای تنظیمات درخواست، مانند روش (GET، POST، PUT، DELETE)، هدرها، و دادههایی که باید ارسال شوند.
مثال ساده از Fetch API
مثال زیر نحوه استفاده از Fetch API برای دریافت داده از یک سرور را نشان میدهد:
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json()) // تبدیل پاسخ به JSON
.then(data => console.log(data)) // نمایش دادهها
.catch(error => console.error('Error:', error)); // مدیریت خطا
در این مثال:
fetch به URL مورد نظر یک درخواست ارسال میکند.
اولین then پاسخ را به فرمت JSON تبدیل میکند.
دومین then دادهها را نمایش میدهد.
catch در صورت بروز خطا، پیام مناسب را در کنسول نشان میدهد.
ویژگیهای کلیدی Fetch API
Promise-based: استفاده از Promises، کد را خواناتر و سادهتر کرده و به جلوگیری از Callback Hell کمک میکند.
پشتیبانی از انواع درخواستها: Fetch API از روشهای مختلف HTTP مانند GET, POST, PUT, DELETE و غیره پشتیبانی میکند.
انعطافپذیری بالا: با تنظیمات مختلف، میتوان هدرها، بدنه درخواست و سایر گزینهها را به راحتی پیکربندی کرد.
مدیریت خطاها: اگرچه Fetch API درخواستهایی که به درستی ارسال شدهاند ولی کد وضعیت HTTP آنها خطا است (مانند 404) را به عنوان موفقیت تلقی میکند، اما همچنان به توسعهدهندگان اجازه میدهد که این نوع خطاها را به شکل موثری شناسایی و مدیریت کنند.
مثال استفاده از Fetch API برای درخواست POST
برای ارسال داده به سرور، باید از روش POST استفاده کرده و دادهها را در بخش بدنه (body) درخواست ارسال کنید.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'My New Post',
body: 'This is the content of the post',
userId: 1
})
})
.then(response => response.json())
.then(data => console.log('Created Post:', data))
.catch(error => console.error('Error:', error));
در این مثال:
method: روش درخواست به POST تغییر داده شده است.
headers: Content-Type مشخص میکند که دادهها به فرمت JSON ارسال میشوند.
body: با استفاده از JSON.stringify، شیء دادهها به یک رشته JSON تبدیل میشود و در بدنه درخواست ارسال میگردد.
مدیریت وضعیت پاسخ (Response Status)
یکی از ویژگیهای Fetch API این است که اگر وضعیت HTTP 404 یا 500 باشد، همچنان Promise به عنوان یک درخواست موفقیتآمیز محسوب میشود. به همین دلیل باید وضعیت پاسخ را بررسی کرده و در صورت نیاز خطا را مدیریت کنید.
مثال:
fetch('https://jsonplaceholder.typicode.com/posts/100')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
در اینجا:
متد ok بررسی میکند که آیا پاسخ با موفقیت برگشته یا خطا رخ داده است.
اگر وضعیت ok نباشد، یک خطا ایجاد و به بخش catch ارسال میشود.
استفاده از Fetch API با async/await
با استفاده از async/await میتوانید کدهای Fetch API را خواناتر کنید و مدیریت خطاها را با try…catch انجام دهید.
مثال:
async function fetchData() {
try {
let response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let data = await response.json();
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchData();
در این مثال:
await منتظر میماند تا fetch پاسخ را دریافت کند.
با استفاده از try…catch، خطاها به راحتی مدیریت میشوند.
تنظیمات بیشتر در Fetch API
Fetch API امکان تنظیمات پیشرفتهای مانند ارسال کوکیها، مدیریت CORS و سایر تنظیمات امنیتی را نیز دارد. برخی از مهمترین تنظیمات در options عبارتاند از:
mode: مشخص میکند که آیا درخواست باید به صورت cors, no-cors یا same-origin باشد.
credentials: تنظیم میکند که کوکیها و اعتبارنامهها در درخواست ارسال شوند یا خیر (same-origin, include, omit).
cache: نحوه رفتار مرورگر با حافظه کش را تعیین میکند (default, no-store, reload, no-cache, force-cache, only-if-cached)
مثال استفاده از برخی تنظیمات پیشرفته:
fetch('https://example.com/data', {
method: 'GET',
mode: 'cors',
credentials: 'include',
cache: 'no-cache'
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
مزایای Fetch API
ساختار ساده و خوانا: fetch به توسعهدهندگان اجازه میدهد تا کد HTTP را به روشی تمیز و مختصر بنویسند.
هماهنگی با Promises: استفاده از Promises در Fetch API، کد را خواناتر و مدیریت خطاها را آسانتر میکند.
پشتیبانی از درخواستهای مدرن و امن: Fetch API از پروتکلهای امنیتی و CORS پشتیبانی میکند که باعث بهبود امنیت درخواستها میشود.
محدودیتهای Fetch API
عدم پشتیبانی از درخواستهای همزمان قدیمی: Fetch API از درخواستهای همزمان یا abort کردن یک درخواست در حال اجرا به صورت پیشفرض پشتیبانی نمیکند، هرچند که میتوان از AbortController برای لغو درخواست استفاده کرد.
محدودیت در مدیریت خطاها: در صورت بروز خطاهای شبکه، Fetch API تنها از TypeError برای تشخیص خطا استفاده میکند و اطلاعات کاملی درباره نوع خطا ارائه نمیدهد.
استفاده از AbortController با Fetch API
AbortController در جاوااسکریپت، یک ابزار مفید برای لغو درخواستهای fetch است که در هنگام نیاز به متوقف کردن یک درخواست در حال اجرا یا جلوگیری از اجرای آن کاربرد دارد.
مثال استفاده از AbortController:
const controller = new AbortController();
const signal = controller.signal;
fetch('https://jsonplaceholder.typicode.com/posts', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Error:', error);
}
});
// لغو درخواست پس از ۲ ثانیه
setTimeout(() => controller.abort(), 2000);
در این مثال، controller.abort() پس از ۲ ثانیه درخواست را لغو میکند.
Fetch API به دلیل پشتیبانی از Promises، نوشتن کدهای HTTP را ساده و ساختار کد را تمیزتر کرده است. از طریق ترکیب Fetch API با async/await و استفاده از AbortController، توسعهدهندگان میتوانند به راحتی عملیات غیرهمزمان HTTP را مدیریت و درخواستها را در زمان لازم لغو کنند.
استفاده از fetch برای درخواستهای HTTP
Fetch API یکی از محبوبترین ابزارهای مدرن برای ارسال درخواستهای HTTP در جاوااسکریپت است که جایگزین XMLHttpRequest شده است. با استفاده از این API، میتوان به راحتی درخواستهای HTTP را به صورت غیرهمزمان ارسال و دادهها را از سرور دریافت یا به آن ارسال کرد. ویژگی اصلی fetch، استفاده از ساختار Promises است که خوانایی و سادگی کد را افزایش میدهد و مشکلات مربوط به Callback Hell را رفع میکند.
نحوه کارکرد Fetch API
متد fetch یک درخواست HTTP به آدرس URL مشخص شده ارسال میکند و یک Promise برمیگرداند. پس از ارسال درخواست، نتیجه به صورت غیرهمزمان دریافت میشود. این Promise شامل یک شیء Response است که وضعیت و دادههای پاسخ سرور را در خود نگه میدارد.
ساختار کلی fetch به صورت زیر است:
fetch(url, options)
.then(response => {
// پردازش پاسخ
})
.catch(error => {
// مدیریت خطا
});
url: آدرس URL سرور که درخواست به آن ارسال میشود.
options: پارامتر اختیاری برای تنظیمات درخواست مانند روش (GET، POST، PUT، DELETE)، هدرها، و دادههایی که باید ارسال شوند.
مثال ساده از درخواست GET با Fetch API
در این مثال، با استفاده از Fetch API یک درخواست GET به یک آدرس URL ارسال و دادهها را به شکل JSON دریافت میکنیم:
fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json()) // تبدیل پاسخ به فرمت JSON
.then(data => console.log(data)) // نمایش دادهها در کنسول
.catch(error => console.error("Error:", error)); // مدیریت خطا
در اینجا:
متد fetch یک Promise برمیگرداند که نتیجه درخواست را به عنوان شیء Response دریافت میکند.
response.json() دادهها را از پاسخ به فرمت JSON تبدیل میکند و یک Promise جدید برمیگرداند.
در نهایت، دادهها در کنسول نمایش داده میشوند.
اگر خطایی رخ دهد، با استفاده از catch آن را مدیریت میکنیم و پیامی مناسب نمایش میدهیم.
گزینههای مختلف در Fetch API
Fetch API به شما این امکان را میدهد که درخواستهای پیچیدهتری ارسال کنید و پارامترهای مختلفی را در options تنظیم کنید. در اینجا مهمترین گزینهها را توضیح میدهیم:
method: روش درخواست (GET, POST, PUT, DELETE و غیره). به صورت پیشفرض GET است.
headers: تنظیم هدرهای HTTP برای درخواست، که میتواند شامل اطلاعاتی مانند نوع داده ارسالی (Content-Type) یا توکنهای احراز هویت باشد.
body: بدنه درخواست که در متدهایی مانند POST و PUT استفاده میشود تا دادهها به سرور ارسال شوند. این بدنه باید به شکل رشته یا فرم باشد.
مثال استفاده از روش POST با Fetch API
در این مثال، یک درخواست POST برای ارسال دادهها به سرور ارسال میشود. بدنه درخواست به صورت JSON ارسال میشود و در هدر نیز نوع محتوا به عنوان JSON تنظیم میگردد.
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'My New Post',
body: 'This is the content of the post',
userId: 1
})
})
.then(response => response.json())
.then(data => console.log('Created Post:', data))
.catch(error => console.error("Error:", error));
در این مثال:
روش POST مشخص شده است.
Content-Type در هدرها تعیین میکند که بدنه درخواست به صورت JSON ارسال میشود.
body حاوی دادههایی است که در قالب یک رشته JSON به سرور ارسال میشوند.
مدیریت وضعیت پاسخ با Fetch API
یکی از ویژگیهای Fetch API این است که اگر پاسخ HTTP با وضعیتهای خطا مانند 404 یا 500 برگردد، همچنان Promise موفقیتآمیز برمیگردد، مگر اینکه مشکل شبکهای وجود داشته باشد. بنابراین، برای مدیریت بهتر خطاها باید وضعیت پاسخ را با response.ok بررسی کنیم.
مثال:
fetch('https://jsonplaceholder.typicode.com/posts/100')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
در اینجا:
اگر response.ok برابر false باشد، یک خطا ایجاد و به catch ارسال میشود.
به این ترتیب، خطاهایی مانند عدم یافتن منابع یا خطاهای سرور به درستی مدیریت میشوند.
استفاده از Fetch API با async/await
با استفاده از async/await، میتوان کدهای Fetch API را به صورت خواناتر نوشت و از try…catch برای مدیریت خطاها استفاده کرد.
مثال:
async function fetchData() {
try {
let response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let data = await response.json();
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchData();
در این مثال:
await منتظر میماند تا fetch نتیجه را برگرداند و سپس دادهها را به JSON تبدیل میکند.
با استفاده از try…catch، خطاها به سادگی و بهصورت خوانا مدیریت میشوند.
تنظیمات امنیتی و دسترسی در Fetch API
Fetch API همچنین دارای گزینههای تنظیم امنیتی مانند mode و credentials است که برای مدیریت سطح دسترسی درخواستها و ارسال کوکیها کاربرد دارند.
mode: مشخص میکند که آیا درخواست باید به صورت cors (برای درخواستهای بیندامنه)، no-cors (محدود به درخواستهای ساده)، یا same-origin (برای همان دامنه) باشد.
credentials: تنظیم میکند که کوکیها و اعتبارنامهها در درخواست ارسال شوند یا خیر. گزینههای این تنظیم عبارتاند از:
same-origin: تنها زمانی که درخواست به همان دامنه ارسال شود، کوکیها را همراهی میکند.
include: در همه درخواستها کوکیها را ارسال میکند.
omit: هیچ کوکی ارسال نمیکند.
مثال:
fetch('https://example.com/data', {
method: 'GET',
mode: 'cors',
credentials: 'include'
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
لغو درخواستها با استفاده از AbortController
با استفاده از AbortController، میتوان درخواستهای Fetch API را در صورت نیاز لغو کرد. این ویژگی زمانی مفید است که یک درخواست طولانی داریم و میخواهیم از اجرای آن جلوگیری کنیم.
مثال:
const controller = new AbortController();
const signal = controller.signal;
fetch('https://jsonplaceholder.typicode.com/posts', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Error:', error);
}
});
// لغو درخواست پس از ۲ ثانیه
setTimeout(() => controller.abort(), 2000);
در این مثال:
یک AbortController ایجاد شده و سیگنال آن به fetch متصل شده است.
با controller.abort() پس از ۲ ثانیه درخواست لغو میشود.
Fetch API به دلیل سهولت استفاده، خوانایی کد و پشتیبانی از عملیات غیرهمزمان با Promises، یکی از بهترین ابزارها برای ارسال درخواستهای HTTP در جاوااسکریپت است. با استفاده از async/await و قابلیت لغو درخواستها با AbortController، توسعهدهندگان میتوانند کدهای تمیزتر و بهینهتری برای درخواستهای HTTP بنویسند.
نتیجه گیری
در این مقاله، با مفاهیم عملیاتهای غیرهمزمان در جاوااسکریپت و ابزارهای مختلفی که برای مدیریت این عملیاتها وجود دارند، آشنا شدیم. Callback Functions به عنوان روشی اولیه برای کنترل عملیاتهای غیرهمزمان معرفی شد، اما Promises و async/await با سادهسازی کد و جلوگیری از مشکلاتی مانند Callback Hell، امکانات پیشرفتهتری برای مدیریت عملیاتهای غیرهمزمان ارائه دادند. سپس به بررسی Fetch API پرداختیم، که یکی از بهترین روشهای ارسال درخواست HTTP در جاوااسکریپت است و با استفاده از ساختار Promiseها، امکان دریافت و ارسال دادهها به سرور را بهصورت ساده و مؤثری فراهم میکند.
Fetch API با قابلیتهایی همچون مدیریت وضعیت پاسخ، پشتیبانی از async/await و ابزارهایی مانند AbortController، امکانات پیشرفتهای برای مدیریت درخواستها و لغو آنها در زمان نیاز فراهم کرده است. در نهایت، setTimeout و setInterval به عنوان ابزارهایی پایهای برای مدیریت زمانبندی و تاخیر در جاوااسکریپت معرفی شدند.
با یادگیری و استفاده از این ابزارها، توسعهدهندگان میتوانند کدهای جاوااسکریپت خود را به صورت بهینهتر و خواناتر مدیریت کرده و عملیاتهای پیچیده غیرهمزمان را بدون ایجاد پیچیدگی اضافه انجام دهند.
