Asynch JS - قوة $.Deferred

من أهم الجوانب المتعلقة بإنشاء تطبيقات HTML5 سلسة وسريعة الاستجابة هي المزامنة بين جميع الأجزاء المختلفة من التطبيق، مثل جلب البيانات ومعالجتها والرسوم المتحرّكة وعناصر واجهة المستخدم.

يكمن الاختلاف الرئيسي بين المتصفحات وبيئة سطح المكتب أو البيئة الأصلية في أنّ المتصفحات لا توفّر إمكانية الوصول إلى نموذج معالجة المهام المتعدّدة، بل توفّر سلسلة مهام واحدة لكل ما يصل إلى واجهة المستخدم (أي نموذج DOM). وهذا يعني أنّ كلّ منطق التطبيق الذي يصل إلى عناصر واجهة المستخدم ويُعدّلها يكون دائمًا في سلسلة المهام نفسها، وبالتالي، فإنّ أهمية الحفاظ على جميع وحدات عمل التطبيق صغيرة وفعّالة قدر الإمكان والاستفادة من أيّ إمكانات غير متزامنة يوفّرها المتصفّح قدر الإمكان.

واجهات برمجة التطبيقات غير المتزامنة للمتصفّح

لحسن الحظ، توفّر المتصفّحات عددًا من واجهات برمجة التطبيقات غير المتزامنة، مثل واجهات برمجة تطبيقات XHR (XMLHttpRequest أو "AJAX") الشائعة الاستخدام، بالإضافة إلى واجهات برمجة تطبيقات IndexedDB وSQLite وWeb workers في HTML5 وواجهات برمجة تطبيقات GeoLocation في HTML5، على سبيل المثال لا الحصر. يتم عرض بعض الإجراءات ذات الصلة بـ DOM بشكل غير متزامن، مثل الصور المتحركة بتنسيق CSS3 من خلال أحداث transitionEnd.

تُعرِض المتصفّحات البرمجة غير المتزامنة لمنطق التطبيق من خلال الأحداث أو وظائف الاستدعاء.
في واجهات برمجة التطبيقات غير المتزامنة المستندة إلى الأحداث، يسجِّل المطوّرون معالِج أحداث لعنصر معيّن (مثل عنصر HTML أو عناصر DOM أخرى)، ثم يطلبون الإجراء. سينفِّذ المتصفّح الإجراء عادةً في سلسلة مهام مختلفة، ويشغِّل الحدث في السلسلة الرئيسية عند الاقتضاء.

على سبيل المثال، سيظهر الرمز الذي يستخدم XHR API، وهي واجهة برمجة تطبيقات غير متزامنة تستند إلى الأحداث، على النحو التالي:

// Create the XHR object to do GET to /data resource  
var xhr = new XMLHttpRequest();
xhr.open("GET","data",true);

// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
alert("We got data: " + xhr.response);
}
},false)

// perform the work
xhr.send();

يُعدّ حدث transitionEnd في CSS3 مثالاً آخر على واجهة برمجة التطبيقات غير المتزامنة المستندة إلى الأحداث.

// get the html element with id 'flyingCar'  
var flyingCarElem = document.getElementById("flyingCar");

// register an event handler 
// ('transitionEnd' for FireFox, 'webkitTransitionEnd' for webkit) 
flyingCarElem.addEventListener("transitionEnd",function(){
// will be called when the transition has finished.
alert("The car arrived");
});

// add the CSS3 class that will trigger the animation
// Note: some browers delegate some transitions to the GPU , but 
//       developer does not and should not have to care about it.
flyingCarElemen.classList.add('makeItFly') 

تعتمد واجهات برمجة تطبيقات المتصفّح الأخرى، مثل SQLite وHTML5 Geolocation، على طلبات إعادة الاتصال، ما يعني أنّ المطوّر يُرسل دالة كوسيطة ستتم إعادة الاتصال بها من خلال التنفيذ الأساسي باستخدام الدقة المقابلة.

على سبيل المثال، بالنسبة إلى تحديد الموقع الجغرافي في HTML5، يبدو الرمز كما يلي:

// call and pass the function to callback when done.
navigator.geolocation.getCurrentPosition(function(position){  
        alert('Lat: ' + position.coords.latitude + ' ' +  
                'Lon: ' + position.coords.longitude);  
});  

في هذه الحالة، ما عليك سوى استدعاء طريقة وضبط دالة ستتمّ استدعاؤها مرة أخرى مع النتيجة المطلوبة. يتيح ذلك للمتصفّح تنفيذ هذه الوظيفة بشكل متزامن أو غير متزامن ومنح المطوّر واجهة برمجة تطبيقات واحدة بغض النظر عن تفاصيل التنفيذ.

إتاحة التطبيقات للاستخدام غير المتزامن

بالإضافة إلى واجهات برمجة التطبيقات غير المتزامنة المضمّنة في المتصفّح، يجب أن تعرِض التطبيقات المصمّمة بشكل جيد واجهات برمجة التطبيقات ذات المستوى المنخفض بطريقة غير متزامنة أيضًا، خاصةً عند تنفيذ أي نوع من عمليات I/O أو المعالجة الحسابية المكثّفة. على سبيل المثال، يجب أن تكون واجهات برمجة التطبيقات للحصول على البيانات غير متزامنة، ويجب ألا تبدو على النحو التالي:

// WRONG: this will make the UI freeze when getting the data  
var data = getData();
alert("We got data: " + data);

يتطلّب تصميم واجهة برمجة التطبيقات هذا حظر getData()، ما سيؤدي إلى تجميد واجهة المستخدم إلى أن يتم استرجاع البيانات. إذا كانت البيانات محلية في سياق JavaScript، قد لا يشكّل ذلك مشكلة، ولكن إذا كان يجب جلب البيانات من الشبكة أو حتى على الجهاز في قاعدة بيانات SQLite أو فهرس، قد يكون لذلك تأثير كبير في تجربة المستخدم.

إنّ التصميم الصحيح هو جعل جميع واجهات برمجة تطبيقات التطبيقات التي قد تستغرق بعض الوقت لمعالجتها غير متزامنة من البداية، لأنّ إعادة تجهيز رمز التطبيق المتزامن ليصبح غير متزامن قد يكون مهمة شاقة.

على سبيل المثال، ستصبح واجهة برمجة التطبيقات البسيطة getData() على النحو التالي:

getData(function(data){
alert("We got data: " + data);
});

ومن المزايا الرائعة لهذا النهج أنّه يفرض على رمز واجهة مستخدم التطبيق أن يكون مركزيًا على الأداء غير المتزامن من البداية ويسمح لواجهات برمجة التطبيقات الأساسية بتحديد ما إذا كان ينبغي أن يكون الأداء غير متزامن أم لا في مرحلة لاحقة.

يُرجى العلم أنّه ليس من الضروري أن تكون جميع واجهات برمجة تطبيقات التطبيق غير متزامنة. وقاعدة القصوى هي أنّ أي واجهة برمجة تطبيقات تُجري أي نوع من عمليات الإدخال/الإخراج أو المعالجة المكثفة (أي عملية يمكن أن تستغرق أكثر من 15 ملي ثانية) يجب أن تكون غير متزامنة من البداية حتى إذا كان التنفيذ الأول متزامنًا.

معالجة حالات الفشل

من عيوب البرمجة غير المتزامنة أنّ طريقة try/catch التقليدية للتعامل مع الأعطال لم تعُد تعمل بشكلٍ جيد، لأنّ الأخطاء تحدث عادةً في سلسلة محادثات أخرى. نتيجةً لذلك، يجب أن يكون لدى المُستلِم طريقة منظَّمة لإعلام المُتصل عند حدوث خطأ أثناء المعالجة.

في واجهة برمجة التطبيقات غير المتزامنة المستندة إلى الأحداث، يتم غالبًا تنفيذ ذلك من خلال رمز التطبيق الذي يُجري طلبات بحث عن الحدث أو العنصر عند تلقّي الحدث. بالنسبة إلى واجهات برمجة التطبيقات التي تعمل بدون اتّصال والتي تستند إلى طلبات إعادة الاتصال، من أفضل الممارسات استخدام وسيطة ثانية تأخذ دالّة يتمّ استدعاؤها في حال حدوث خطأ مع تضمين معلومات الخطأ المناسبة كوسيطة.

سيبدو طلب getData على النحو التالي:

// getData(successFunc,failFunc);  
getData(function(data){
alert("We got data: " + data);
}, function(ex){
alert("oops, some problem occured: " + ex);
});

تجميع العناصر معًا باستخدام $.Deferred

من بين قيود أسلوب الاستدعاء أعلاه أنّه قد يصبح من الصعب جدًا كتابة منطق مزامنة متقدم إلى حدٍ ما.

على سبيل المثال، إذا كنت بحاجة إلى الانتظار حتى تكتمل عمليتان غير متزامنتَين لواجهة برمجة التطبيقات قبل تنفيذ عملية ثالثة، يمكن أن تزيد تعقيدات الرمز البرمجي بسرعة.

// first do the get data.   
getData(function(data){
// then get the location
getLocation(function(location){
alert("we got data: " + data + " and location: " + location);
},function(ex){
alert("getLocation failed: "  + ex);
});
},function(ex){
alert("getData failed: " + ex);
});

يمكن أن تصبح الأمور أكثر تعقيدًا عندما يحتاج التطبيق إلى إجراء المكالمة نفسها من أجزاء متعددة من التطبيق، لأنّ كل مكالمة ستضطر إلى تنفيذ هذه المكالمات المتعدّدة الخطوات، أو سيضطر التطبيق إلى تنفيذ آلية التخزين المؤقت الخاصة به.

لحسن الحظ، هناك نمط قديم نسبيًا يُعرف باسم "الوعود" (يشبه نوعًا ما Future في Java) وتنفيذ قوي وحديث في نواة jQuery يُعرف باسم $.Deferred، وهو يقدّم حلًا بسيطًا وفعّالاً للبرمجة غير المتزامنة.

لتبسيط الأمر، يحدّد نمط "الوعود" أنّ واجهة برمجة التطبيقات غير المتزامنة تُرجع ملفًا شخصيًا لـ "الوعد" يمثّل نوعًا من "الوعد بأنّه سيتم حلّ النتيجة باستخدام البيانات المقابلة". للحصول على الحلّ، يحصل المُرسِل على ملف الوعد ويُجري done(successFunc(data)) الذي سيطلب من ملف الوعد استدعاء successFunc هذا عند حلّ "البيانات".

وبالتالي، يصبح مثال طلب getData أعلاه على النحو التالي:

// get the promise object for this API  
var dataPromise = getData();

// register a function to get called when the data is resolved
dataPromise.done(function(data){
alert("We got data: " + data);
});

// register the failure function
dataPromise.fail(function(ex){
alert("oops, some problem occured: " + ex);
});

// Note: we can have as many dataPromise.done(...) as we want. 
dataPromise.done(function(data){
alert("We asked it twice, we get it twice: " + data);
});

هنا، نحصل على عنصر dataPromise أولاً ثم نستدعي الطريقة .done لتسجيل دالة نريد استدعاؤها مرة أخرى عند حلّ البيانات. ويمكننا أيضًا استدعاء طريقة .fail لمعالجة الفشل النهائي. يُرجى العلم أنّه يمكننا إجراء أي عدد من طلبات .done أو .fail حسب الحاجة لأنّ عملية تنفيذ Promise الأساسية (رمز jQuery) ستتحكّم في التسجيل وطلبات الاستدعاء.

باستخدام هذا النمط، من السهل نسبيًا تنفيذ رمز برمجي أكثر تقدمًا للتزامن، ويقدّم jQuery حاليًا الرمز الأكثر شيوعًا مثل $.when.

على سبيل المثال، سيتحوّل المُعلِم المُدمَج getData/getLocation أعلاه إلى ما يلي:

// assuming both getData and getLocation return their respective Promise
var combinedPromise = $.when(getData(), getLocation())

// function will be called when both getData and getLocation resolve
combinePromise.done(function(data,location){
alert("We got data: " + dataResult + " and location: " + location);
});  

والميزة الرائعة في ذلك هي أنّ jQuery.Deferred يسهّل على المطوّرين تنفيذ الدالة غير المتزامنة. على سبيل المثال، يمكن أن يظهر getData على النحو التالي:

function getData(){
// 1) create the jQuery Deferred object that will be used
var deferred = $.Deferred();

// ---- AJAX Call ---- //
XMLHttpRequest xhr = new XMLHttpRequest();
xhr.open("GET","data",true);

// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
    // 3.1) RESOLVE the DEFERRED (this will trigger all the done()...)
    deferred.resolve(xhr.response);
}else{
    // 3.2) REJECT the DEFERRED (this will trigger all the fail()...)
    deferred.reject("HTTP error: " + xhr.status);
}
},false) 

// perform the work
xhr.send();
// Note: could and should have used jQuery.ajax. 
// Note: jQuery.ajax return Promise, but it is always a good idea to wrap it
//       with application semantic in another Deferred/Promise  
// ---- /AJAX Call ---- //

// 2) return the promise of this deferred
return deferred.promise();
}

لذلك، عند استدعاء getData()‎، يتم أولاً إنشاء عنصر jQuery.Deferred جديد (1) ثم عرض الوعد (2) لكي يتمكّن المُرسِل من تسجيل الدالتَين done وfail . بعد ذلك، عندما يعود طلب XHR، يتم حلّ الطلب المؤجّل (3.1) أو رفضه (3.2). سيؤدي تنفيذ deferred.resolve إلى تنشيط جميع وظائف done(…) ووظائف الوعد الأخرى (مثل then وpipe)، وسيؤدي استدعاء deferred.reject إلى استدعاء جميع وظائف fail()‎.

حالات الاستخدام

في ما يلي بعض حالات الاستخدام الجيدة التي يمكن أن يكون فيها "العرض المؤجّل" مفيدًا جدًا:

الوصول إلى البيانات: يكون التصميم المناسب في الغالب هو عرض واجهات برمجة التطبيقات للوصول إلى البيانات بتنسيق $.Deferred. وهذا واضح بالنسبة إلى البيانات البعيدة، حيث قد تؤدي المكالمات المتزامنة عن بُعد إلى تدمير تجربة المستخدم تمامًا، ولكنه ينطبق أيضًا على البيانات المحلية كما هو الحال في غالبًا واجهات برمجة التطبيقات ذات المستوى الأدنى (على سبيل المثال، SQLite وIndexedDB) غير متزامنَين. إنّ دالة $.when و.pipe في Deferred API هما أداتان فعّالتان للغاية لمزامنة طلبات البحث الفرعية غير المتزامنة وربطها ببعضها.

الصور المتحركة لواجهة المستخدم: يمكن أن تكون عملية تنسيق صورة متحركة واحدة أو أكثر باستخدام أحداث transitionEnd مملة للغاية، خاصةً عندما تكون الصور المتحركة عبارة عن مزيج من الصور المتحركة CSS3 وJavaScript (كما هو الحال غالبًا). يمكن أن يؤدي تضمين دوالّ الرسوم المتحركة في Deferred إلى تقليل تعقيد الرمز البرمجي بشكل كبير وتحسين المرونة. حتى دالة برنامج تضمين عامة بسيطة مثل cssAnimation(className) التي ستعرض كائن Promise الذي تم حله عند transferEnd يمكن أن تكون مساعدة كبيرة.

عرض مكوّنات واجهة المستخدم: هذه ميزة أكثر تقدمًا قليلاً، ولكن يجب أن تستخدم إطارات عمل مكوّنات HTML المتقدمة ميزة "العرض المؤجّل" أيضًا. بدون الخوض كثيرًا في التفاصيل (سيكون هذا موضوع مشاركة أخرى)، عندما يحتاج التطبيق إلى عرض أجزاء مختلفة من واجهة المستخدم، فإنّ تضمين دورة حياة تلك المكوّنات في Deferred يتيح التحكّم بشكل أكبر في التوقيت.

أي واجهة برمجة تطبيقات غير متزامنة مع المتصفّح: لأغراض التسوية، غالبًا ما يكون من المفيد التفاف طلبات البيانات من واجهة برمجة التطبيقات للمتصفّح على أنّها مؤجلة. يستغرق ذلك من 4 إلى 5 أسطر من التعليمات البرمجية لكل منها، ولكن سيبسّط ذلك أي رمز برمجي للتطبيق. كما هو موضّح في رمز getData/getLocation الزائف أعلاه، يتيح رمز التطبيقات أن يكون لرمز التطبيقات نموذج واحد غير متزامن في جميع أنواع واجهات برمجة التطبيقات (المتصفحات وتفاصيل التطبيق والمركبة المركّبة).

التخزين المؤقت: هذه إحدى النتائج الجانبية، ولكنّها قد تكون مفيدة جدًا في بعض الحالات. لأنّ واجهات برمجة التطبيقات Promise (مثل يمكن استدعاء دالّتَي ‎.done(…) و‎ .fail(…)) قبل أو بعد تنفيذ الطلب غير المتزامن، ويمكن استخدام العنصر Deferred كاسم تخزين مؤقت لطلب غير متزامن. على سبيل المثال، يمكن أن يتتبّع CacheManager الطلبات المحدّدة التي تتضمّن Deferred، ويعرض وعدًا بالاستجابة لطلب Deferred المطابق إذا لم يتم إبطاله. يكمن جمال هذه الطريقة في أنّه ليس على المتصل معرفة ما إذا تم حلّ المكالمة أو ما إذا كانت في مرحلة حلّ، لأنّه سيتم استدعاء وظيفة معاودة الاتصال بالطريقة نفسها تمامًا.

الخاتمة

على الرغم من أنّ مفهوم $.Deferred بسيط، إلا أنّه قد يستغرق بعض الوقت للتمكّن من استخدامه بشكل جيد. ومع ذلك، ونظرًا لطبيعة بيئة المتصفّح، فإنّ إتقان البرمجة غير المتزامنة في JavaScript هو أمر ضروري لأي مطوّر تطبيقات HTML5 جاد، وتشكل بنية Promise (وتنفيذ jQuery) أدوات رائعة لتطوير برمجة غير متزامنة موثوقة وفعّالة.