مقدمة في البرمجة غير المتزامنة في JavaScript
JavaScript هي لغة برمجة أحادية الخيط مع نموذج إدخال/إخراج غير محظور (non-blocking I/O). هذا يعني أنه في أي لحظة زمنية يتم تنفيذ عملية واحدة فقط. ومع ذلك، بفضل البرمجة غير المتزامنة، يمكن لـ Node.js معالجة آلاف الطلبات في وقت واحد دون حظر الخيط الرئيسي. في هذه المقالة، سنشرح المفاهيم الأساسية لعدم التزامن: الاستدعاءات (callbacks)، والوعود (Promises)، وبناء الجملة async/await.
ما هو عدم التزامن ولماذا هو مطلوب؟
تخيل أنك تطلب بيتزا. إذا وقفت عند المنضدة وانتظرت حتى يتم تحضيرها - فهذا هو الكود المتزامن (synchronous). كل شيء آخر يتوقف. النهج غير المتزامن (asynchronous) - تترك الطلب وتنشغل بأمورك الخاصة. عندما تصبح البيتزا جاهزة، يتم استدعاؤك. JavaScript تعمل بهذه الطريقة تمامًا: إنها لا تنتظر اكتمال العملية (قراءة ملف، طلب إلى خادم)، بل تستمر في تنفيذ الكود التالي. عند اكتمال العملية، يتم استدعاء دالة خاصة - الاستدعاء (callback).
في Node.js، تعتبر عدم التزامن مهمة بشكل خاص لأن الخادم يجب أن يخدم العديد من العملاء في وقت واحد. بدونها، كان كل طلب جديد سيحظر عمل التطبيق بأكمله.
الكود المتزامن مقابل الكود غير المتزامن: مثال
لننظر إلى مثال بسيط لقراءة ملف. النسخة المتزامنة تحظر التنفيذ:
const fs = require('fs');
// قراءة متزامنة - تحظر الخيطconst data = fs.readFileSync('file.txt', 'utf8');console.log('تمت قراءة البيانات:', data);console.log('هذا الكود سيتم تنفيذه فقط بعد قراءة الملف');النسخة غير المتزامنة لا تحظر:
const fs = require('fs');
// قراءة غير متزامنة - لا تحظرfs.readFile('file.txt', 'utf8', (err, data) => { if (err) throw err; console.log('تمت قراءة البيانات:', data);});
console.log('هذا الكود سيتم تنفيذه فورًا، دون انتظار قراءة الملف');الطرق الرئيسية للعمل مع عدم التزامن
في JavaScript (و Node.js)، هناك ثلاث طرق رئيسية للبرمجة غير المتزامنة. سننظر إليها من الأقدم إلى الأحدث.
1. Callback (دوال الاستدعاء)
الاستدعاء (callback) هو دالة يتم تمريرها إلى دالة أخرى ويتم استدعاؤها بعد اكتمال العملية غير المتزامنة. هذه هي الطريقة الأساسية والأقدم.
function getUserData(userId, callback) { setTimeout(() => { const user = { id: userId, name: 'آنا' }; callback(null, user); // الوسيط الأول - الخطأ (إن وجد) }, 1000);}
getUserData(1, (err, user) => { if (err) { console.error('خطأ:', err); return; } console.log('المستخدم:', user.name);});المشكلة: عندما تكثر العمليات، يتحول الكود إلى "جحيم الاستدعاءات" (callback hell) - دوال متداخلة بعمق يصعب قراءتها وتصحيح أخطائها.
2. Promise (الوعود)
الوعد (Promise) هو كائن يمثل نتيجة عملية غير متزامنة. يمكن أن يكون في واحدة من ثلاث حالات:
- pending (قيد الانتظار) - العملية لم تكتمل بعد
- fulfilled (مُنجز) - اكتملت العملية بنجاح
- rejected (مرفوض) - اكتملت العملية بخطأ
الوعود تحل مشكلة callback hell وتجعل الكود أكثر خطية:
function getUserData(userId) { return new Promise((resolve, reject) => { setTimeout(() => { const user = { id: userId, name: 'إيفان' }; // resolve - نجاح، reject - خطأ resolve(user); // reject(new Error('المستخدم غير موجود')); }, 1000); });}
// استخدام سلسلة .then()getUserData(1) .then(user => { console.log('المستخدم:', user.name); return user.id; }) .then(id => console.log('المعرف:', id)) .catch(err => console.error('خطأ:', err)) .finally(() => console.log('اكتملت العملية'));