البرمجة غير المتزامنة في JavaScript
JavaScript أحادي الخيط — يمكنه فعل شيء واحد فقط في كل مرة. لكن إذا استغرق طلب الشبكة 3 ثواني، هل يجب أن تتجمد الصفحة لـ 3 ثواني؟ بالطبع لا. البرمجة غير المتزامنة تحل هذا: بدلاً من الانتظار، تأخذ رقماً وتعود عندما يكون جاهزاً. مثل نظام طابور المطعم — لا تقف عند باب المطبخ، بل تجلس وتنتظر استدعاء رقمك.
متزامن مقابل غير متزامن
متزامن: الكود ينفذ سطر بسطر — السطر التالي ينتظر انتهاء الحالي. مثل الوقوف في الصف — تنتظر حتى ينتهي الشخص أمامك.
غير متزامن: عمليات معينة (طلبات الشبكة، المؤقتات، إدخال/إخراج الملفات) تبدأ دون عرض الكود اللاحق. عندما تنتهي، يتم إخطارك عبر استدعاء أو Promise.
<script>
console.log('1');
setTimeout(function() {
console.log('2');
}, 1000);
console.log('3');
</script>
ترتيب الإخراج هو 1 → 3 → 2. setTimeout غير متزامن — الاستدعاء ينفذ بعد ثانية واحدة دون عرض console.log('3').
الاستدعاءات (Callbacks)
الاستدعاءات هي النمط الأساسي للبرمجة غير المتزامنة — تمرير دالة كمعامل، وتُستدعى عندما تكتمل العملية غير المتزامنة.
مثال: استخدام أساسي للاستدعاء
<div id="output" style="padding: 10px; border: 1px solid #ccc;"></div>
<script>
const output = document.getElementById('output');
function fetchData(callback) {
output.textContent = 'جاري التحميل...';
setTimeout(function() {
callback('تم تحميل البيانات!');
}, 2000);
}
fetchData(function(data) {
output.textContent = data;
});
</script>
جحيم الاستدعاءات (Callback Hell)
عندما تعتمد العمليات غير المتزامنة على بعضها تسلسلياً، تتداخل الاستدعاءات أعمق وأعمق — هذا هو "جحيم الاستدعاءات" سيئ السمعة.
<script>
getUser(function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
getItems(detail.itemId, function(items) {
console.log('4 مستويات عميقة — أضف تعقيداً أكثر ويصبح غير مقروء');
});
});
});
});
</script>
Promise
Promise هو كائن يمثل النتيجة النهائية لعملية غير متزامنة. لديه ثلاث حالات:
| الحالة | الوصف |
|---|---|
pending |
قيد التنفيذ، لا نتيجة بعد |
fulfilled |
اكتملت بنجاح، النتيجة متاحة |
rejected |
فشلت، الخطأ متاح |
بمجرد انتقال Promise من pending إلى fulfilled أو rejected، لا تتغير مرة أخرى — مثل سهم أُطلق، لا يمكن استدعاؤه.
إنشاء Promise
<script>
const promise = new Promise(function(resolve, reject) {
// عملية غير متزامنة
// استدعاء resolve(result) عند النجاح
// استدعاء reject(error) عند الفشل
});
</script>
then / catch / finally
<script>
promise
.then(function(result) { console.log(result); }) // ينفذ عند النجاح
.catch(function(error) { console.error(error); }) // ينفذ عند الفشل
.finally(function() { console.log('تم'); }); // ينفذ بغض النظر
</script>
مثال: محاكاة طلب بـ Promise
<button id="loadBtn">تحميل البيانات</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');
function fetchUser() {
return new Promise(function(resolve, reject) {
output.textContent = 'جاري التحميل...';
setTimeout(function() {
const success = true;
if (success) {
resolve({ name: 'أليس', age: 25 });
} else {
reject(new Error('فشل التحميل'));
}
}, 1500);
});
}
btn.addEventListener('click', function() {
fetchUser()
.then(function(user) {
output.textContent = 'الاسم: ' + user.name + '، العمر: ' + user.age;
})
.catch(function(err) {
output.textContent = 'خطأ: ' + err.message;
})
.finally(function() {
console.log('اكتمل الطلب (نجاح أو فشل)');
});
});
</script>
التسلسل
then نفسها تُرجع Promise، لذا يمكنك تسلسل الاستدعاءات — جحيم الاستدعاءات يصبح خطاً مباشراً.
<script>
fetchUser()
.then(function(user) { return fetchOrders(user.id); })
.then(function(orders) { return fetchDetail(orders[0].id); })
.then(function(detail) { console.log(detail); })
.catch(function(err) { console.error(err); });
</script>
catch واحدة في نهاية التسلسل تلتقط الأخطاء من أي خطوة — لا تحتاج catch بعد كل then.
async / await
async/await هو سكر صناعي فوق Promises — يجعل الكود غير المتزامن يبدو متزامناً، مما يحسن القراءة بشكل كبير.
async: يعلن دالة غير متزامنة تُرجع Promise تلقائياًawait: يوقف التنفيذ حتى تحل Promise، ثم يُرجع النتيجة
مثال: إعادة كتابة الطلب أعلاه بـ async/await
<button id="loadBtn">تحميل البيانات</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadBtn');
const output = document.getElementById('output');
function fetchUser() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ name: 'بوب', age: 30 });
}, 1500);
});
}
btn.addEventListener('click', async function() {
try {
output.textContent = 'جاري التحميل...';
const user = await fetchUser();
output.textContent = 'الاسم: ' + user.name + '، العمر: ' + user.age;
} catch (err) {
output.textContent = 'خطأ: ' + err.message;
} finally {
console.log('اكتمل الطلب');
}
});
</script>
async/await يحول "خذ رقماً وانتظر" إلى "ابقَ في الصف دون مغادرة" — تدفق الكود مباشقر والقراءة ترتفع بشكل كبير.
معالجة الأخطاء
async/await يستخدم try/catch لمعالجة الأخطاء — نفس النمط المستخدم مع الكود المتزامن.
<script>
async function loadAll() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
console.log(orders);
} catch (err) {
console.error('حدث خطأ ما:', err);
}
}
</script>
Promise.all و Promise.race
| الطريقة | الوصف |
|---|---|
Promise.all([p1, p2, p3]) |
ينجح فقط إذا نجح الجميع؛ يفشل بمجرد فشل واحد |
Promise.race([p1, p2, p3]) |
يستخدم نتيجة أيهما يحل أولاً (نجاح أو فشل) |
مثال: طلبات متزامنة مع Promise.all
<button id="loadAll">تحميل متزامن</button>
<div id="output" style="padding: 10px; border: 1px solid #ccc; margin-top: 10px;"></div>
<script>
const btn = document.getElementById('loadAll');
const output = document.getElementById('output');
function delay(ms, value) {
return new Promise(function(resolve) {
setTimeout(function() { resolve(value); }, ms);
});
}
btn.addEventListener('click', async function() {
output.textContent = 'جاري التحميل...';
const results = await Promise.all([
delay(1000, 'بيانات المستخدم'),
delay(1500, 'بيانات الطلبات'),
delay(800, 'بيانات التكوين')
]);
output.textContent = 'تم تحميل الكل:\n' + results.join(' | ');
});
const raceBtn = document.createElement('button');
raceBtn.textContent = 'تحميل سباق';
document.body.appendChild(raceBtn);
raceBtn.addEventListener('click', async function() {
const fastest = await Promise.race([
delay(1000, 'المصدر A (1 ثانية)'),
delay(500, 'المصدر B (0.5 ثانية)'),
delay(800, 'المصدر C (0.8 ثانية)')
]);
output.textContent = 'الأسرع: ' + fastest;
});
</script>
Promise.all مثل عشاء جماعي — يجب أن يصل الجميع قبل أن تأكل. Promise.race مثل سباق — من ينتهي أولاً يفوز. استخدم all للطلبات المستقلة لتسريع الأشياء، و race عندما يكون لديك عدة مصادر مرآة وتريد الأسرع.
📖 ملخص
- الكود المتزامن يعرض التنفيذ اللاحق؛ غير المتزامن لا يعرض — أحادي الخيط لا يزال يمكنه "فعل أشياء متعددة"
- الاستدعاءات هي النمط الأساسي؛ التداخل العميق يخلق جحيم الاستدعاءات
- Promise لديه ثلاث حالات:
pending → fulfilledأوpending → rejected— انتقالات الحالة لا رجعة فيها then/catch/finallyتستهلك Promises؛ التسلسل يحل جحيم الاستدعاءاتasync/awaitهو سكر صناعي فوق Promises؛awaitيمكن استخدامه فقط داخل دوالasyncPromise.allينتظر نجاح الجميع؛Promise.raceيأخذ أول من يحل
❓ أسئلة شائعة
await في دالة عادية؟await يمكن استخدامه فقط داخل دالة async. استخدام await في دالة عادية يسبب خطأ صيغة. await المسطح مدعوم في الوحدات، لكن الدعم في المتصفحات يختلف.Promise.all؟Promise.all يرفض فوراً بالخطأ الأول. Promises الأخرى لا تزال تُنفذ، لكن نتائجها تُتجاهل. إذا كنت بحاجة إلى "الجميع يحل بغض النظر عن النتيجة"، استخدم Promise.allSettled.async؟async تُرجع دائماً Promise. حتى لو return 42، في الواقع تُرجع Promise.resolve(42). لذا يمكنك استخدام .then() لاستهلاك النتيجة.📝 تمارين
- أساسي: استخدم
Promise+setTimeoutلمحاكاة تأخير ثانيتين، ثم اطبع "تم" عند النجاح. - متوسط: أعد كتابة أعلاه بـ
async/awaitوأضف معالجة أخطاءtry/catch. - تحدي: استخدم
Promise.allلإطلاق 3 طلبات محاكاة بتأخيرات مختلفة (1 ثانية/2 ثانية/3 ثانية) متزامنة، ثم احسب واعرض الوقت الكلي المنقضي عندما تكتمل جميعها.



