برمجة JavaScript غير المتزامنة واستدعاءات رد الاتصال
عدم التزامن في لغات البرمجة
تعتبر أجهزة الكمبيوتر غير متزامنة بحكم تصميمها.
يعني عدم التزامن أن الأمور يمكن أن تحدث بشكل مستقل عن تدفق البرنامج الرئيسي.
في أجهزة الكمبيوتر الاستهلاكية الحالية، يتم تشغيل كل برنامج لفترة زمنية محددة ثم يتوقف تنفيذه للسماح لبرنامج آخر بمواصلة تنفيذه. يحدث هذا الشيء في دورة سريعة جدًا بحيث يستحيل ملاحظتها. نعتقد أن أجهزة الكمبيوتر الخاصة بنا تقوم بتشغيل العديد من البرامج في وقت واحد، ولكن هذا وهم (باستثناء الأجهزة متعددة المعالجات).
تستخدم البرامج داخليًا المقاطعات، وهي إشارة يتم إرسالها إلى المعالج لجذب انتباه النظام.
دعونا لا ندخل في التفاصيل الداخلية لهذا الآن، ولكن ضع في اعتبارك فقط أنه من الطبيعي أن تكون البرامج غير متزامنة وتوقف تنفيذها حتى تحتاج إلى اهتمام، مما يسمح للكمبيوتر بتنفيذ أشياء أخرى في هذه الأثناء. عندما ينتظر برنامج استجابة من الشبكة، لا يمكنه إيقاف المعالج حتى ينتهي الطلب.
عادةً ما تكون لغات البرمجة متزامنة ويوفر البعض طريقة لإدارة عدم التزامن في اللغة أو من خلال المكتبات. تعتبر C و Java و C# و PHP و Go و Ruby و Swift و Python جميعها متزامنة بشكل افتراضي. يتعامل البعض مع العمليات غير المتزامنة باستخدام سلاسل العمليات، أو إنشاء عملية جديدة.
JavaScript
JavaScript هي لغة متزامنة بشكل افتراضي وأحادية الترابط. هذا يعني أن التعليمات البرمجية لا يمكنها إنشاء سلاسل عمليات جديدة وتشغيلها بالتوازي.
يتم تنفيذ سطور التعليمات البرمجية في سلسلة، واحدًا تلو الآخر، على سبيل المثال:
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();
ولكن JavaScript ولدت داخل المتصفح، وكانت وظيفتها الرئيسية، في البداية، هي الاستجابة لإجراءات المستخدم، مثل onClick
و onMouseOver
و onChange
و onSubmit
وما إلى ذلك. كيف يمكن أن تفعل ذلك مع نموذج برمجة متزامن؟
كانت الإجابة في بيئتها. يوفر المتصفح طريقة للقيام بذلك من خلال توفير مجموعة من واجهات برمجة التطبيقات التي يمكنها التعامل مع هذا النوع من الوظائف.
في الآونة الأخيرة، قدمت Node.js بيئة إدخال/إخراج غير حظر لتوسيع هذا المفهوم ليشمل الوصول إلى الملفات واستدعاءات الشبكة وما إلى ذلك.
ردود الاتصال (Callbacks)
لا يمكنك معرفة متى سينقر المستخدم على زر. لذلك، تقوم بتحديد معالج أحداث لحدث النقر. يقبل معالج الأحداث هذا دالة، والتي سيتم استدعاؤها عند تشغيل الحدث:
document.getElementById('button').addEventListener('click', () => {
// تم النقر على العنصر
});
هذا ما يسمى رد الاتصال (callback).
رد الاتصال هو دالة بسيطة يتم تمريرها كقيمة إلى دالة أخرى، ولن يتم تنفيذها إلا عند وقوع الحدث. يمكننا القيام بذلك لأن JavaScript لديها دوال من الدرجة الأولى، والتي يمكن تعيينها لمتغيرات وتمريرها إلى دوال أخرى (تسمى الدوال ذات الرتبة العليا)
من الشائع تغليف كل التعليمات البرمجية الخاصة بك على جانب العميل في مستمع حدث load على كائن window، والذي يقوم بتشغيل دالة رد الاتصال فقط عندما تكون الصفحة جاهزة:
window.addEventListener('load', () => {
// تم تحميل النافذة
// افعل ما تريد
});
تُستخدم ردود الاتصال في كل مكان، وليس فقط في أحداث DOM.
أحد الأمثلة الشائعة هو استخدام المؤقتات:
setTimeout(() => {
// يعمل بعد ثانيتين
}, 2000);
تقبل طلبات XHR أيضًا رد اتصال، في هذا المثال عن طريق تعيين دالة لخاصية سيتم استدعاؤها عند وقوع حدث معين (في هذه الحالة، تتغير حالة الطلب):
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error');
}
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();
معالجة الأخطاء في ردود الاتصال
كيف تتعامل مع الأخطاء باستخدام ردود الاتصال؟ إحدى الاستراتيجيات الشائعة جدًا هي استخدام ما تبنته Node.js: المعامل الأول في أي دالة رد اتصال هو كائن الخطأ: ردود الاتصال التي تبدأ بالخطأ (error-first callbacks)
إذا لم يكن هناك خطأ، فسيكون الكائن فارغًا (null). إذا كان هناك خطأ، فإنه يحتوي على بعض وصف الخطأ ومعلومات أخرى.
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
if (err) {
// معالجة الخطأ
console.log(err);
return;
}
// لا توجد أخطاء، معالجة البيانات
console.log(data);
});
مشكلة ردود الاتصال
ردود الاتصال رائعة للحالات البسيطة!
ولكن كل رد اتصال يضيف مستوى من التداخل، وعندما يكون لديك الكثير من ردود الاتصال، يبدأ الكود في أن يصبح معقدًا جدًا بسرعة:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// هنا الكود الخاص بك
});
}, 2000);
});
});
هذا مجرد رمز بسيط ذي 4 مستويات، لكنني رأيت مستويات تداخل أكثر من ذلك بكثير وهذا ليس ممتعًا.
كيف نحل هذا؟
بدائل لردود الاتصال
بدءًا من ES6، قدمت JavaScript العديد من الميزات التي تساعدنا في التعليمات البرمجية غير المتزامنة التي لا تتضمن استخدام ردود الاتصال: Promises
(ES6) و Async/Await
(ES2017).