JavaScript غير المتزامن: المؤقّتات وPromise وfetch
درس 8 من 8
8JavaScript غير المتزامن: المؤقّتات وPromise وfetch
الصورة الكبيرةكل تطبيق حقيقي يكلّم خادمًا: يجلب قائمة المنتجات، يرسل الطلب، يحدّث الرصيد. والشبكة بطيئة وغير مضمونة. لو انتظرت الصفحة الرد وهي متجمّدة لما استطعت حتى التمرير. هذا الدرس يعلّمك كيف تطلب شيئًا الآن وتتصرّف حين يصل، وهو الجسر بين الواجهة التي بنيتها والخادم الذي ستبنيه في المسار التالي.
ماذا تشاهد في الرسم: الطلب يخرج للشبكة، والصفحة تبقى حيّة تستجيب. حين يصل الرد، يُكمل الكود من بعد await: يقرأ البيانات ثم يرسمها، أو يُظهر رسالة خطأ إن فشل.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
تخيّل أنك تطلب قهوة في مقهى مزدحم. الطريقة المتزامنة: تقف أمام الكاشير ولا يخدم أحدًا غيرك حتى تجهز قهوتك، والطابور كله ينتظر. الطريقة غير المتزامنة: يأخذ طلبك ويعطيك جهاز تنبيه (هذا هو الـ Promise، أي «وعد» بقهوة لاحقًا)، وتجلس وتكمل حديثك، والكاشير يخدم غيرك. حين يهتز الجهاز (الوعد تحقّق) تقوم وتأخذ قهوتك. وأحيانًا يهتز ليقول «نفد الحليب» (الوعد رُفض)، فتتصرّف.
JavaScript في المتصفح خيط واحد (single-threaded) مع حلقة أحداث (event loop): مكدّس الاستدعاءات (call stack) ينفّذ الكود المتزامن حتى يفرغ، ثم تُسحب المهام من الطوابير: طابور المهام الدقيقة (microtasks: استجابات الوعود وqueueMicrotask) يُفرَّغ كاملًا قبل كل مهمة كبيرة (macrotask: المؤقّتات، أحداث الواجهة، ردود الشبكة). Promise كائن بثلاث حالات: pending ثم fulfilled أو rejected، وسلسلة .then/.catch/.finally. async تجعل الدالة تعيد Promise دائمًا، وawait تعلّق الدالة (لا الخيط) حتى يستقر الوعد، والرفض يصير استثناءً يُلتقط بـ try/catch. fetch يعيد Promise يستقر بـ Response حتى مع 404 و500، فافحص res.ok؛ ويُرفض فقط عند فشل الشبكة أو CORS أو الإلغاء بـ AbortController. Promise.all يشغّل بالتوازي ويفشل بفشل أي واحد، وPromise.allSettled ينتظر الكل.
قبل أن تبدأ
🧰الدوال والمصفوفات والكائنات
تعرف الدوال السهمية وcallbacks وJSON.
🖱️JavaScript والـ DOM
تعدّل الصفحة وتستمع للأحداث.
📨HTTP
تعرف الطلب والرد ورموز الحالة مثل 200 و404.
الفكرة الجوهرية
1لماذا «غير متزامن»؟ حلقة الأحداث
JavaScript ينفّذ سطرًا واحدًا في كل لحظة. لو كان كل انتظار يوقف كل شيء، لتجمّدت الصفحة ثانيتين في كل طلب للخادم. الحل: المهام البطيئة تُسلَّم للمتصفح (أو Node) ليتابعها، والكود يكمل. حين تنتهي، تدخل في طابور، وحلقة الأحداث تنفّذها حين يفرغ الكود الحالي.
setTimeout(fn, ms) أبسط مثال: «نادِ هذه الدالة بعد كذا مللي ثانية». شغّل المثال وتوقّع الترتيب قبل أن ترى الناتج:
console.log("1. take the order");
setTimeout(() => {
console.log("4. coffee ready (after 300 ms)");
}, 300);
setTimeout(() => {
console.log("3. receipt printed (after 0 ms, but still waits its turn)");
}, 0);
console.log("2. serve the next customer");السطر 3 جاء بعد 2 رغم أن مؤقّته صفر! لأن أي مؤقّت ينتظر حتى ينتهي الكود الحالي كله، ثم تأخذه حلقة الأحداث من الطابور. هذه الفكرة وحدها تفسّر نصف ألغاز JavaScript.
2Promise: وعد بقيمة لاحقًا
الـ Promise كائن يمثّل نتيجة لم تصل بعد. له ثلاث حالات: قيد الانتظار (pending)، ثم إما تحقّق (fulfilled) بقيمة، أو رُفض (rejected) بخطأ. تكتب ماذا تفعل حين يتحقّق بـ .then(fn)، وحين يُرفض بـ .catch(fn)، وفي الحالتين بـ .finally(fn).
function brew(drink) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (drink === "tea") reject(new Error("we only serve coffee"));
else resolve(`your ${drink} is ready`);
}, 200);
});
}
console.log("ordered, waiting...");
brew("latte")
.then((msg) => console.log("OK:", msg))
.catch((err) => console.log("FAILED:", err.message))
.finally(() => console.log("buzzer returned"));
brew("tea")
.then((msg) => console.log("OK:", msg))
.catch((err) => console.log("FAILED:", err.message));new Promise كثيرًا بنفسك. أغلب الوقت تأتيك الوعود جاهزة من دوال مثل fetch، ودورك أن تنتظرها وتتعامل مع نتيجتها. كتبناها هنا لترى ما بداخلها.3async وawait: كود غير متزامن يُقرأ كالعادي
سلاسل .then تصير صعبة القراءة حين تطول. الحل الحديث: اكتب async قبل الدالة، ثم await قبل أي Promise: «انتظر هنا حتى يصل الرد، ثم أكمل». الدالة تتوقف مؤقتًا، لكن الصفحة لا تتوقف. ومعالجة الخطأ بـ try { ... } catch (err) { ... } كأي كود عادي.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// a pretend server: answers after a short delay
async function getMenu() {
await wait(150);
return [{ name: "latte", price: 14 }, { name: "v60", price: 18 }];
}
async function getStock(name) {
await wait(100);
if (name === "v60") throw new Error("v60 beans are out of stock");
return 12;
}
async function showMenu() {
try {
const menu = await getMenu();
console.log("menu:", menu.map((d) => d.name).join(", "));
for (const drink of menu) {
const stock = await getStock(drink.name);
console.log(drink.name, "stock:", stock);
}
} catch (err) {
console.log("could not finish:", err.message);
} finally {
console.log("done loading");
}
}
showMenu();
console.log("the page is still responsive while we wait");await. const menu = getMenu() بلا await يعطيك Promise لا المصفوفة، فيقول menu.map is not a function. وإن رأيت [object Promise] على الشاشة فهذا هو السبب غالبًا.4fetch: اطلب بيانات من الخادم
fetch(url) يرسل طلب HTTP ويعيد Promise برد (Response). بعدها await res.json() يقرأ الجسم ويحوّله من JSON لكائن. انتبه: fetch يعتبر 404 و500 «نجاحًا» لأن الخادم ردّ، لذلك افحص res.ok بنفسك. ولإرسال بيانات استخدم method: "POST" وbody بـ JSON.
// GET: read data
async function loadMenu() {
const res = await fetch("https://api.example.com/menu");
if (!res.ok) throw new Error(`server said ${res.status}`);
return res.json();
}
// POST: send data
async function placeOrder(order) {
const res = await fetch("https://api.example.com/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(order),
});
if (!res.ok) throw new Error(`order failed: ${res.status}`);
return res.json(); // e.g. { id: 42, status: "received" }
}في الملعب التالي نجلب بيانات حقيقية بـ fetch داخل المتصفح. حتى يعمل بلا إنترنت، يطلب «رابط بيانات» (data URL) فيه JSON بدل خادم. في مشروعك تضع رابط الـ API الحقيقي مكانه والكود نفسه لا يتغيّر:
fetch حقيقي في المتصفح
اضغط الزر. ثم غيّر URL إلى "https://invalid.invalid/menu" واضغط مرة ثانية لترى فرع الخطأ يعمل.
5حالات الواجهة: تحميل وخطأ وفراغ ونجاح
كل شاشة تجلب بيانات لها أربع حالات يجب أن تصمّمها كلها، لا حالة النجاح فقط: التحميل (أظهر مؤشّرًا وعطّل الزر حتى لا يُضغط مرتين)، الخطأ (رسالة مفهومة وزر «حاول مجددًا»)، الفراغ (لا نتائج: قل ذلك بوضوح)، والنجاح. المستخدم الذي يرى شاشة بيضاء لا يعرف هل ينتظر أم يغادر.
جرّب الحالات الأربع
اضغط البحث عاديًا، ثم فعّل «يفشل»، ثم «فارغة». لاحظ أن الزر يتعطّل أثناء التحميل. النصوص هنا ثابتة من الكود، لذا innerHTML آمن؛ مع نص من المستخدم استخدم textContent.
6عدة طلبات معًا: Promise.all
إن احتجت ثلاثة أشياء مستقلة (القائمة، العروض، الفروع)، فانتظارها واحدًا بعد الآخر يجمع أوقاتها. Promise.all([...]) يبدؤها كلها معًا وينتظرها، فالوقت الكلي يصير وقت أبطئها فقط. وإن فشل واحد فشل الكل، فإن أردت نتيجة كل واحد مهما حدث استخدم Promise.allSettled.
const wait = (ms, value) => new Promise((r) => setTimeout(() => r(value), ms));
const getMenu = () => wait(300, ["latte", "mocha"]);
const getOffers = () => wait(200, ["2 for 1 on Fridays"]);
const getBranches = () => wait(200, ["Olaya", "Malqa"]);
async function oneByOne() {
const t = Date.now();
await getMenu(); await getOffers(); await getBranches();
return Date.now() - t;
}
async function allTogether() {
const t = Date.now();
const [menu, offers, branches] = await Promise.all([getMenu(), getOffers(), getBranches()]);
console.log(menu.length, "drinks,", offers.length, "offer,", branches.length, "branches");
return Date.now() - t;
}
(async () => {
const slow = await oneByOne();
const fast = await allTogether();
console.log("one by one ~", Math.round(slow / 100) * 100, "ms");
console.log("together ~", Math.round(fast / 100) * 100, "ms");
})();مثال محلول
المسألة: صفحة طلب قهوة تجلب القائمة، والمستخدم يختار ويضغط «اطلب» فيُرسل الطلب، مع تعطيل الزر أثناء الإرسال ورسالة نجاح برقم الطلب أو رسالة خطأ. الحل: دالتان async لكل طلب، وحالة واحدة تُرسم منها الواجهة. الخادم هنا مُحاكى بـ wait حتى يعمل المثال بلا إنترنت:
صفحة طلب كاملة غير متزامنة
اطلب لاتيه ثم V60 لترى النجاح والخطأ. في درس Express ستكتب الخادم الحقيقي الذي يحلّ مكان api هنا.
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| async/await يجعل JavaScript متعدد الخيوط. | ما زال خيطًا واحدًا. await يعلّق الدالة فقط ويترك حلقة الأحداث تشغّل غيرها. |
| fetch يرمي خطأ إن ردّ الخادم 404. | يعتبره ردًّا ناجحًا. افحص res.ok بنفسك وارمِ الخطأ. |
| setTimeout(fn, 0) ينفّذ فورًا. | ينتظر انتهاء الكود الحالي كله ثم يأخذ دوره في الطابور. |
أين تقابله في العمل
🛒تحميل المنتجات والطلبات
كل قائمة تراها في متجر جاءت من fetch.
💬الإشعارات والدردشة
انتظار رسائل جديدة دون تجميد الصفحة.
🧑🏫المعلّم الذكي في درب
يرسل سؤالك للخادم وينتظر الرد بـ await.
اختبر فهمك
ما ترتيب الطباعة: console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C");؟
المؤقّت ينتظر انتهاء الكود الحالي حتى لو كان صفرًا.
كتبت const data = fetch(url) بلا await. ماذا في data؟
fetch يعيد Promise فورًا؛ await هو ما ينتظر قيمته.
الخادم ردّ 500. ماذا يفعل fetch؟
الخادم ردّ فعلًا، فالوعد يتحقّق، ودورك فحص res.ok.
أين تلتقط خطأ await؟
الوعد المرفوض يصير استثناءً عند await، ويُلتقط بـ catch.
ثلاثة طلبات مستقلة يأخذ كل منها ثانية. كم الوقت مع Promise.all؟
تبدأ معًا، فالوقت الكلي وقت أبطئها.
بطاقات مراجعة سريعة
تحقق من فهمك
لماذا لا تتجمّد الصفحة أثناء fetch؟
لأن المتصفح يتابع الطلب، والكود يكمل، وحين يصل الرد تضعه حلقة الأحداث في الطابور.
ما الفرق بين رفض الوعد وres.ok = false؟
الرفض يعني أن الطلب لم يصل أو لم يُرد عليه (شبكة)، وres.ok = false يعني أن الخادم ردّ برمز خطأ كـ 404.
لماذا نعطّل الزر أثناء الإرسال؟
حتى لا يضغطه المستخدم مرتين فيُرسل طلبين، وليعرف أن شيئًا يحدث.
متى تستخدم Promise.all بدل await متتالية؟
حين تكون الطلبات مستقلة لا يحتاج أحدها نتيجة الآخر.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي1
كتب1
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.