برمجة 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 بيئة I/O غير مُعيقة لتوسيع هذا المفهوم إلى الوصول إلى الملفات، وعمليات الاتصال بالشبكة وما إلى ذلك.
وظائف الاستدعاء (Callbacks)
لا يمكنك معرفة متى سينقر المستخدم على زر. لذلك، تقوم بتعريف مُعالِج حدث (event handler) لحدث النقر. يقبل هذا المُعالِج دالة، سيتم استدعاؤها عند تشغيل الحدث:
document.getElementById('button').addEventListener('click', () => {
// تم النقر على العنصر
});
هذه هي ما يسمى بـ وظيفة الاستدعاء (callback).
وظيفة الاستدعاء هي دالة بسيطة تُمرر كقيمة إلى دالة أخرى، ولن يتم تنفيذها إلا عند حدوث الحدث. يمكننا القيام بذلك لأن جافا سكريبت لديها دوال من الدرجة الأولى (first-class functions)، والتي يمكن تعيينها للمتغيرات وتمريرها إلى دوال أخرى (تسمى الدوال من الرتبة الأعلى (higher-order functions))
من الشائع تغليف كل رمز العميل الخاص بك في مُستمع حدث load على كائن window، والذي يقوم بتشغيل دالة الاستدعاء فقط عندما يكون الصفحة جاهزة:
window.addEventListener('load', () => {
// تم تحميل النافذة
// افعل ما تريد
});
تُستخدم وظائف الاستدعاء في كل مكان، وليس فقط في أحداث DOM.
مثال شائع هو استخدام المؤقتات:
setTimeout(() => {
// يتم التشغيل بعد 2 ثانية
}, 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);
});
مشكلة المُستَدْعيات (Callbacks)
المُستَدْعيات رائعة للحالات البسيطة!
ومع ذلك، فإن كل مُستَدْعَى يضيف مستوى من التداخل، وعندما يكون لديك الكثير من المُستَدْعيات، يبدأ الكود في التعقيد بسرعة كبيرة:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// your code here
});
}, 2000);
});
});
هذا مجرد كود بسيط من 4 مستويات، لكنني رأيت مستويات أكثر بكثير من التداخل وهذا ليس ممتعًا.
كيف نحل هذه المشكلة؟
بدائل للمُستَدْعيات (Callbacks)
بدءًا من ES6، قدمت جافا سكريبت العديد من الميزات التي تساعدنا مع الكود غير المتزامن الذي لا يتضمن استخدام المُستَدْعيات: Promises
(ES6) و Async/Await
(ES2017).