یکی از مهمترین جنبه های ساخت برنامه های کاربردی HTML5 روان و پاسخگو، همگام سازی بین تمام بخش های مختلف برنامه مانند واکشی داده ها، پردازش، انیمیشن ها و عناصر رابط کاربری است.
تفاوت اصلی با دسکتاپ یا محیط بومی این است که مرورگرها به مدل threading دسترسی نمی دهند و برای هر چیزی که به رابط کاربری (یعنی DOM) دسترسی دارد، یک رشته ارائه می دهد. این بدان معنی است که تمام منطق برنامه برای دسترسی و اصلاح عناصر رابط کاربری همیشه در یک رشته قرار دارند، از این رو اهمیت کوچک و کارآمد نگه داشتن تمام واحدهای کاری برنامه تا حد ممکن و استفاده از هر گونه قابلیت ناهمزمانی که مرورگر ارائه می دهد، اهمیت دارد. ممکن است.
APIهای ناهمزمان مرورگر
خوشبختانه، مرورگرها تعدادی API ناهمزمان مانند APIهای XHR (XMLHttpRequest یا 'AJAX') و همچنین IndexedDB، SQLite، HTML5 Web Workers و APIهای HTML5 GeoLocation را ارائه می دهند. حتی برخی از اقدامات مربوط به DOM به صورت ناهمزمان در معرض نمایش قرار می گیرند، مانند انیمیشن CSS3 از طریق رویدادهای transitionEnd.
روشی که مرورگرها برنامهنویسی ناهمزمان را در معرض منطق برنامه قرار میدهند، از طریق رویدادها یا تماسهای برگشتی است.
در APIهای ناهمزمان مبتنی بر رویداد، توسعه دهندگان یک کنترل کننده رویداد را برای یک شی معین (مثلاً عنصر HTML یا سایر اشیاء DOM) ثبت می کنند و سپس اقدام را فراخوانی می کنند. مرورگر این عمل را معمولاً در رشتهای متفاوت انجام میدهد و در صورت لزوم، رویداد را در رشته اصلی فعال میکند.
برای مثال، کد با استفاده از XHR API، یک 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 transitionEnd نمونه دیگری از API ناهمزمان مبتنی بر رویداد است.
// 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')
سایر APIهای مرورگر، مانند SQLite و HTML5 Geolocation، مبتنی بر callback هستند، به این معنی که توسعهدهنده تابعی را به عنوان آرگومان ارسال میکند که توسط پیادهسازی زیربنایی با وضوح مربوطه فراخوانی میشود.
به عنوان مثال، برای HTML5 Geolocation، کد به شکل زیر است:
// call and pass the function to callback when done.
navigator.geolocation.getCurrentPosition(function(position){
alert('Lat: ' + position.coords.latitude + ' ' +
'Lon: ' + position.coords.longitude);
});
در این حالت، ما فقط یک متد را فراخوانی می کنیم و تابعی را ارسال می کنیم که با نتیجه درخواستی دوباره فراخوانی می شود. این به مرورگر اجازه می دهد تا این عملکرد را به صورت همزمان یا ناهمزمان پیاده سازی کند و یک API واحد را بدون توجه به جزئیات پیاده سازی به توسعه دهنده بدهد.
آماده سازی برنامه های کاربردی به صورت ناهمزمان
فراتر از API های ناهمزمان داخلی مرورگر، برنامه های کاربردی با معماری خوب باید API های سطح پایین خود را نیز به صورت ناهمزمان در معرض نمایش قرار دهند، به خصوص زمانی که هر نوع I/O یا پردازش سنگین محاسباتی را انجام می دهند. به عنوان مثال، APIها برای دریافت داده باید ناهمزمان باشند و نباید شبیه به این باشند:
// WRONG: this will make the UI freeze when getting the data
var data = getData();
alert("We got data: " + data);
این طراحی API مستلزم مسدود شدن ()getData است که تا زمانی که دادهها واکشی شوند، رابط کاربری مسدود میشود. اگر دادهها در زمینه جاوا اسکریپت محلی باشند، ممکن است مشکلی ایجاد نشود، اما اگر دادهها باید از شبکه یا حتی به صورت محلی در یک SQLite یا ذخیرهسازی ایندکس واکشی شوند، این میتواند تأثیر چشمگیری بر تجربه کاربر داشته باشد.
طراحی درست این است که فعالانه همه API برنامههایی را که پردازش آنها مدتی طول میکشد، از همان ابتدا ناهمزمان کنیم، زیرا اصلاح کد برنامه همزمان برای ناهمزمان میتواند کار دلهرهآوری باشد.
به عنوان مثال، API ساده getData() چیزی شبیه به:
getData(function(data){
alert("We got data: " + data);
});
نکته خوب در مورد این رویکرد این است که کد UI برنامه را مجبور می کند از ابتدا ناهمزمان محور باشد و به API های زیربنایی اجازه می دهد تصمیم بگیرند که آیا نیاز به ناهمزمان بودن یا نبودن آنها در مرحله بعد دارند.
توجه داشته باشید که همه APIهای برنامه نیاز ندارند یا نباید ناهمزمان باشند. قاعده کلی این است که هر API که هر نوع I/O یا پردازش سنگینی را انجام میدهد (هر کاری که میتواند بیش از 15 میلیثانیه طول بکشد) باید از ابتدا بهصورت ناهمزمان در معرض دید قرار گیرد، حتی اگر اولین پیادهسازی همزمان باشد.
رسیدگی به شکست ها
یکی از ویژگیهای برنامهنویسی ناهمزمان این است که روش سنتی try/catch برای مدیریت خرابیها دیگر واقعاً کار نمیکند، زیرا خطاها معمولاً در رشتهای دیگر اتفاق میافتند. در نتیجه، تماس گیرنده باید روشی ساختاریافته برای اطلاع تماس گیرنده در هنگام بروز مشکل در طول پردازش داشته باشد.
در یک API ناهمزمان مبتنی بر رویداد، این اغلب توسط کد برنامه کاربردی که رویداد یا شی را هنگام دریافت رویداد جستجو میکند، انجام میشود. برای APIهای ناهمزمان مبتنی بر فراخوانی، بهترین روش داشتن یک آرگومان دوم است که تابعی را می گیرد که در صورت خرابی با اطلاعات خطای مناسب به عنوان آرگومان فراخوانی می شود.
تماس getData ما به این شکل خواهد بود:
// getData(successFunc,failFunc);
getData(function(data){
alert("We got data: " + data);
}, function(ex){
alert("oops, some problem occured: " + ex);
});
قرار دادن آن با $.Deferred
یکی از محدودیتهای رویکرد برگشت به تماس بالا این است که نوشتن منطق هماهنگسازی حتی نسبتاً پیشرفته میتواند واقعاً دشوار باشد.
برای مثال، اگر قبل از انجام سومین API باید منتظر بمانید تا دو API ناهمزمان انجام شود، پیچیدگی کد میتواند به سرعت افزایش یابد.
// 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);
});
هنگامی که برنامه نیاز به برقراری تماس یکسان از بخشهای مختلف برنامه داشته باشد، همه چیز حتی پیچیدهتر میشود، زیرا هر تماسی باید این تماسهای چند مرحلهای را انجام دهد، یا برنامه باید مکانیزم ذخیرهسازی خود را پیادهسازی کند.
خوشبختانه، یک الگوی نسبتا قدیمی به نام Promises (نوعی شبیه به Future در جاوا) و یک پیاده سازی قوی و مدرن در هسته جی کوئری به نام $.Deferred وجود دارد که راه حلی ساده و قدرتمند برای برنامه نویسی ناهمزمان ارائه می دهد.
برای سادهتر کردن آن، الگوی Promises تعریف میکند که API ناهمزمان یک شی 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) ایجاد می کند و سپس Promise (2) آن را برمی گرداند تا تماس گیرنده بتواند توابع انجام شده و شکست خود را ثبت کند. سپس، هنگامی که فراخوانی XHR برمی گردد، یا معوق (3.1) را حل می کند یا آن را رد می کند (3.2). انجام deferred.resolve تمام توابع done(…) و دیگر توابع وعده (به عنوان مثال، then و pipe) را راه اندازی می کند و فراخوانی deferred.reject همه توابع ()fail را فراخوانی می کند.
موارد استفاده
در اینجا چند مورد استفاده خوب وجود دارد که Deferred می تواند بسیار مفید باشد:
دسترسی به داده: نمایش APIهای دسترسی به داده به عنوان $.Deferred اغلب طراحی درستی است. این امر برای داده های راه دور واضح است، زیرا تماس های از راه دور همزمان تجربه کاربر را کاملاً خراب می کند، اما برای داده های محلی نیز صادق است زیرا اغلب API های سطح پایین تر (مانند SQLite و IndexedDB) خود ناهمزمان هستند. $.when و .pipe Deferred API برای همگامسازی و زنجیرهسازی پرسشهای فرعی ناهمزمان بسیار قدرتمند هستند.
انیمیشنهای رابط کاربری: تنظیم یک یا چند انیمیشن با رویدادهای transitionEnd میتواند بسیار خستهکننده باشد، بهویژه زمانی که انیمیشنها ترکیبی از انیمیشن CSS3 و جاوا اسکریپت باشند (همانطور که اغلب اتفاق میافتد). بسته بندی توابع انیمیشن به صورت Deferred می تواند پیچیدگی کد را به میزان قابل توجهی کاهش دهد و انعطاف پذیری را بهبود بخشد. حتی یک تابع پیچیده عمومی ساده مانند cssAnimation (className) که شی Promise را که در transitionEnd حل میشود برمیگرداند، میتواند کمک بزرگی باشد.
UI Component Display: این کمی پیشرفتهتر است، اما چارچوبهای پیشرفته HTML Component باید از Deferred نیز استفاده کنند. بدون پرداختن زیاد به جزئیات (این موضوع در پست دیگری خواهد بود)، هنگامی که یک برنامه نیاز به نمایش بخشهای مختلف رابط کاربری دارد، داشتن چرخه عمر آن اجزای کپسولهشده در Deferred امکان کنترل بیشتر زمانبندی را فراهم میکند.
هر API ناهمزمان مرورگر: برای هدف عادی سازی، اغلب ایده خوبی است که فراخوانی های API مرورگر را به صورت Deferred قرار دهید. این به معنای واقعی کلمه هر کدام به 4 تا 5 خط کد نیاز دارد، اما هر کد کاربردی را تا حد زیادی ساده می کند. همانطور که در کد شبه getData/getLocation بالا نشان داده شده است، این به کد برنامهها اجازه میدهد تا یک مدل ناهمزمان در همه انواع API (مرورگرها، ویژگیهای برنامه، و ترکیب) داشته باشند.
ذخیره سازی: این یک مزیت جانبی است، اما در برخی موارد می تواند بسیار مفید باشد. از آنجایی که API های Promise (به عنوان مثال، .done(…) و .fail(…)) را می توان قبل یا بعد از انجام فراخوانی ناهمزمان فراخوانی کرد، شی Deferred می تواند به عنوان یک دسته کش برای یک تماس ناهمزمان استفاده شود. برای مثال، یک CacheManager فقط میتواند Deferred را برای درخواستهای داده شده پیگیری کند و اگر Promise مربوط به Deferred را باطل نکرده باشد، بازگرداند. زیبایی این است که تماس گیرنده مجبور نیست بداند که آیا تماس قبلاً حل شده است یا در حال حل شدن است، عملکرد برگشت تماس آن دقیقاً به همان روش فراخوانی می شود.
نتیجه گیری
در حالی که مفهوم $.Deferred ساده است، ممکن است زمان ببرد تا بتوان آن را کنترل کرد. با این حال، با توجه به ماهیت محیط مرورگر، تسلط بر برنامه نویسی ناهمزمان در جاوا اسکریپت برای هر توسعه دهنده جدی برنامه HTML5 ضروری است و الگوی Promise (و پیاده سازی jQuery) ابزار فوق العاده ای برای قابل اعتماد و قدرتمند کردن برنامه نویسی ناهمزمان است.