Asynch JS - قوة $.Deferred

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

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

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

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

فالطريقة التي تعرض بها المتصفحات البرمجة غير المتزامنة لمنطق التطبيق هي من خلال الأحداث أو عمليات معاودة الاتصال.
في واجهات برمجة التطبيقات غير المتزامنة المستندة إلى الأحداث، يسجّل المطوّرون معالج أحداث لعنصر معيّن (على سبيل المثال، عنصر 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();

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

// 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);  
});  

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

جعل التطبيقات جاهزة بشكل غير متزامن

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

// 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 ملي ثانية) يجب أن يتم عرضها بشكلٍ غير متزامن من البداية حتى لو كانت عملية التنفيذ الأولى متزامنة.

تعذُّر معالجة المشاكل

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

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

ستبدو مكالمة 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);
});

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

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

لتبسيط الأمر، يحدد نمط Promis أنّ واجهة برمجة التطبيقات غير المتزامنة تعرض عنصر Promise وهو نوع من "الوعد بأن تُحل النتيجة بالبيانات المتجاوبة". للحصول على الحل، يحصل المتصل على عنصر Promise ويستدعي done(successFunc(data) الذي سيخبر كائن Promise باستدعاء هذه البيانات 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) حتى يتمكن المتصل من تسجيل دوال jQuery.Deferred الجديدة والإخفاق. بعد ذلك، عند عرض استدعاء XHR، يؤدي إما إلى حل التأجيل (3.1) أو رفضه (3.2). سيؤدي إجراء deferred.resolve إلى تشغيل جميع دوال القيمة (...) ووظائف الوعد الأخرى (مثل ثم والممر) واستدعاء deferred.reject جميع دوال تعذّر().

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

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

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

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

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

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

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

الخلاصة

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