Asynch JS — сила $.Deferred

Одним из наиболее важных аспектов создания плавных и отзывчивых приложений HTML5 является синхронизация между всеми различными частями приложения, такими как получение, обработка данных, анимация и элементы пользовательского интерфейса.

Основное различие между настольным компьютером и собственной средой заключается в том, что браузеры не предоставляют доступ к модели потоков и предоставляют один поток для всего, что обращается к пользовательскому интерфейсу (т. е. к DOM). Это означает, что вся логика приложения, осуществляющая доступ к элементам пользовательского интерфейса и изменяющая их, всегда находится в одном потоке, поэтому важно сохранять все рабочие единицы приложения как можно меньшими и эффективными, а также максимально эффективно использовать любые асинхронные возможности, предлагаемые браузером. возможный.

Асинхронные API браузера

К счастью, браузеры предоставляют ряд асинхронных API, таких как широко используемые API XHR (XMLHttpRequest или AJAX), а также IndexedDB, SQLite, веб-работники HTML5 и API-интерфейсы HTML5 GeoLocation, и это лишь некоторые из них. Даже некоторые действия, связанные с DOM, выполняются асинхронно, например анимация CSS3, через события переходаEnd.

Браузеры предоставляют асинхронное программирование логике приложения через события или обратные вызовы.
В асинхронных 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();

Событие TransitionEnd CSS3 — еще один пример асинхронного 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, основаны на обратном вызове. Это означает, что разработчик передает функцию в качестве аргумента, которая будет вызвана базовой реализацией с соответствующим разрешением.

Например, для геолокации HTML5 код выглядит следующим образом:

// 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 асинхронно, особенно когда они выполняют какой-либо ввод-вывод или тяжелую вычислительную обработку. Например, API для получения данных должны быть асинхронными и НЕ должны выглядеть так:

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

Этот дизайн API требует блокировки getData() , что заморозит пользовательский интерфейс до тех пор, пока данные не будут получены. Если данные являются локальными в контексте JavaScript, это может не быть проблемой, однако, если данные необходимо получить из сети или даже локально в хранилище SQLite или индексе, это может оказать существенное влияние на взаимодействие с пользователем.

Правильный дизайн — заранее сделать все API приложений, обработка которых может занять некоторое время, асинхронными с самого начала, поскольку переоборудование синхронного кода приложения в асинхронный может оказаться сложной задачей.

Например, упрощенный API getData() будет выглядеть примерно так:

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

Преимущество этого подхода заключается в том, что он с самого начала заставляет код пользовательского интерфейса приложения быть асинхронно-ориентированным и позволяет базовым API решать, должны ли они быть асинхронными или нет на более позднем этапе.

Обратите внимание, что не все API приложений должны быть асинхронными. Эмпирическое правило заключается в том, что любой API, который выполняет любой тип ввода-вывода или тяжелую обработку (все, что может занять более 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, прежде чем выполнять третий, сложность кода может быстро возрасти.

// 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 в Java) и надежная и современная реализация в ядре jQuery под названием $.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), а затем возвращает свое обещание (2), чтобы вызывающая сторона могла зарегистрировать свои функции выполнения и сбоя. Затем, когда вызов XHR возвращается, он либо разрешает отложенный вызов (3.1), либо отклоняет его (3.2). Выполнение deferred.resolve вызовет все функции Done(…) и другие функции Promise (например, then и Pipe), а вызов deferred.reject вызовет все функцииfail().

Варианты использования

Вот несколько хороших случаев использования, когда Deferred может быть очень полезен:

Доступ к данным. Представление API доступа к данным как $.Deferred часто является правильным решением. Это очевидно для удаленных данных, поскольку синхронные удаленные вызовы полностью испортили бы взаимодействие с пользователем, но это также справедливо и для локальных данных, поскольку часто API-интерфейсы нижнего уровня (например, SQLite и IndexedDB) сами по себе асинхронны. $.when и .pipe отложенного API чрезвычайно эффективны для синхронизации и объединения асинхронных подзапросов.

Анимации пользовательского интерфейса: оркестровка одной или нескольких анимаций с помощью событий переходаEnd может быть довольно утомительной, особенно если анимация представляет собой смесь CSS3-анимации и JavaScript (как это часто бывает). Обертывание функций анимации как отложенных может значительно снизить сложность кода и повысить гибкость. Даже простая универсальная функция-обертка, такая как cssAnimation(className), которая будет возвращать объект Promise, который разрешается при переходеEnd, может оказаться очень полезным.

Отображение компонентов пользовательского интерфейса: это немного более сложный вариант, но продвинутые платформы HTML-компонентов также должны использовать Deferred. Не вдаваясь в подробности (это будет темой другого поста), когда приложению необходимо отображать различные части пользовательского интерфейса, инкапсуляция жизненного цикла этих компонентов в Deferred позволяет лучше контролировать время.

Любой асинхронный API браузера. В целях нормализации часто рекомендуется обернуть вызовы API браузера как отложенные. Это занимает буквально от 4 до 5 строк кода каждая, но значительно упростит любой код приложения. Как показано в приведенном выше псевдокоде getData/getLocation, это позволяет коду приложения иметь одну асинхронную модель для всех типов API (браузеров, особенностей приложения и соединения).

Кэширование: это своего рода побочное преимущество, но в некоторых случаях оно может оказаться очень полезным. Поскольку API-интерфейсы Promise (например, .done(…) и .fail(…)) можно вызывать до или после выполнения асинхронного вызова, объект Deferred можно использовать в качестве дескриптора кэширования для асинхронного вызова. Например, CacheManager может просто отслеживать Deferred для заданных запросов и возвращать Promise соответствующего Deferred, если он не был признан недействительным. Прелесть в том, что вызывающему объекту не обязательно знать, был ли вызов уже разрешен или находится в процессе разрешения, его функция обратного вызова будет вызвана точно таким же образом.

Заключение

Хотя концепция $.Deferred проста, может потребоваться время, чтобы хорошо с ней разобраться. Однако, учитывая природу среды браузера, освоение асинхронного программирования на JavaScript является обязательным для любого серьезного разработчика приложений HTML5, а шаблон Promise (и реализация jQuery) являются потрясающими инструментами, позволяющими сделать асинхронное программирование надежным и мощным.