Долго сидел и думал, какое же дать название этой статье, т.к. тема гораздо обширнее, чем просто "вернуть ответ из функции Ajax-запросов". Сюда можно было бы приклеить и "Работа с полученным ответом на Ajax-запрос", и "Универсальная функция Ajax-запросов", и "Работа с объектами Promise и Deferred" , и еще массу вариантов. Но всё это сводится к одной проблеме, которая изо дня в день поднимается новичками на форумах и которую можно определить одной фразой - асинхронность выполнения Ajax-запросов.
Что же вообще это значит: "синхронное или асинхронное выполнение"? Если не особо углубляясь и, как говорится, на пальцах, то:
- Синхронное выполнение - это, когда вы сели на горшок справлять нужду, то штаны не натягиваете, пока не завершите этот процесс, а потом не воспользуетесь туалетной бумагой, а может быть еще и биде. ;)
- Асинхронное выполнение - это, когда вы поставили чайник на огонь, включили стиральную машинку и сели читать книгу. Все процессы выполняются независимо друг от друга и не дожидаясь окончания одним другого.
Надеюсь, что суть уловили. :) Давайте разберем типичный ошибочный пример:
function ajaxRequest() {
var response = false;
$.ajax({
url: '/path/to/handler.php',
/* прочие настройки */
success: function (data) {
response = data;
}
});
return response;
}
var something = ajaxRequest();
И вопрос обычно звучит в таком стиле: "Почему функция не возвращает данные? Почему всегда false?" Теперь уже, когда вы знаете разницу между синхронным и асинхронным выполнением, вы можете без труда понять, что происходит внутри функци "ajaxRequest": инициализировали переменную "response", начался процесс ajax-запроса, но еще до его завершения, функция уже возвращает значение переменной, которое так и не успело изменится.
Как с этим бороться? Первое, что может прийти на ум и будет решение не таким уж правильным - это сделать запрос синхронным. В методе $.ajax(), за это отвечает параметр async со значение false (по умолчанию - значение true). Главный минус такого подхода в том, что на время выполнения запроса, сайт у пользователя попросту "подвиснет", а это не есть хорошо. Кроме того, это может отрицательно сказаться на выполнении каких-либо фоновых операциях. Например, если у вас есть функция, которая каждую секунду должна что-то выполнять, то на время Ajax-запроса, она так же зависнет. Поэтому синхронное выполнение, мы отложим для особых случаев, где без синхронности обойтись нельзя.
Универсальная функция для Ajax - это хорошо, но не всегда такую функцию можно сделать под конкретный проект, а понятие "универсальность" в широком смысле, тут вообще не подходит. Как минимум, ответ обрабатывается по разному, запросы могут передавать как обычные данные, так и файлы, где настройки запроса отличаются, могут отличаться функции beforeSend, complete и т.д. В этом случае, можно упростить код за счет метода $.ajaxSetup(). Основные настройки или настройки, которые будут действовать в большинстве случаев, мы можем указать в коде один раз и больше не прописывать их в методе $.ajax().
$.ajaxSetup({
url: '/path/to/handler.php',
type: 'POST',
dataType: 'json',
beforeSend: function() {
$('.loader').show();
},
complete: function() {
$('.loader').hide();
},
error: function(jqXHR, textStatus, errorThrown) {
console.log('Ошибка: ' + textStatus + ' | ' + errorThrown);
}
});
Теперь в любом месте, где нужно сделать ajax-запрос, мы прописываем только то, что отправляем и обработчик ответа:
$.ajax({
data: {val1: 1, val2: 2},
success: function(data){
// обрабатываем ответ
}
});
Если же в каком-то исключительном случае, нам нужно изменить определенную настройку, то её так же дописываем. Например, в каком-то случае, нам не нужно показывать "loader" ( ) перед отправкой запроса или вместо loader-а, вывести что-то в консоль:
$.ajax({
beforeSend: false,
data: {val1: 1, val2: 2},
success: function(data){
// обрабатываем ответ
}
});
$.ajax({
beforeSend: function(){
console.log('Запрос стартует');
},
data: {val1: 1, val2: 2},
success: function(data){
// обрабатываем ответ
}
});
Это был один из вариантов, с помощью которого можно сделать код более универсальным. Теперь же перейдём непосредственно к функциям.
Вариант первый - передача в функцию обработчика ответа на Ajax-запрос:
var mainContent = $('#main'),
sidebarContent = $('#sidebar'),
actions = {
updateContent: function (response) {
mainContent.html(response);
},
updateSidebar: function (response) {
sidebarContent.html(response);
}
};
function ajaxRequest(data, responseHandler) {
$.ajax({
url: '/handler.php',
type: 'POST',
data: {
upd: data
},
success: actions[responseHandler]
});
}
ajaxRequest(1,'updateContent'); // вызов функции на обновление элемента #main
ajaxRequest(2,'updateSidebar'); // вызов функции на обновление элемента #sidebar
Итак, предположим, что у нас на странице есть два элемента, объект "actions", содержащий функции для обработки данных ответа и одна функция ajax-запросов, которая принимает два аргумента: "data" - какие-то данные, передаваемые на сервер и "responseHandler" - имя функции, которая должна обработать ответ сервера (аргументов, понятное дело, что может быть и больше). После успешного завершения запроса, вызывается соответствующая функция из объекта "actions", которая принимает полученные от сервера данные (переменная "response") и заменяет ими контент соответствующего элемента. Если нам понадобится еще какой-то обработчик, то мы просто допишем в объект "actions" новую функцию, при этом что-либо изменять дополнительно уже не понадобится.
В примере, мы вручную записывали имя функции-обработчика, но это можно так же автоматизировать. Например, используя атрибут тегов "data-*":
HTML
<button class="update-something" data-action="updateContent" data-id="1">Обновить контент</button>
<button class="update-something" data-action="updateSidebar" data-id="2">Обновить сайдбар</button>
JS/jQuery
$('.update-something').on('click', function(){
var $that = $(this);
ajaxRequest($that.data('id'), $that.data('action'));
});
В итоге, у нас одна функция для Ajax-запросов, один расширяемый объект с обработчиками, один обработчик кликов для всех кнопок, а вот результат разный и тот, который нужен нам ;)
Вариант второй - используем JavaScript-объект Promise ("обещание"), которой служит для отложенных и асинхронных вычислений. Проще говоря, с помощью этого объекта мы можем установить обработчик события на выполнение какой-либо задачи. В jQuery существует свой объект для работы с "обещаниями" - это объект $.Deferred(). Разбирать его по косточкам не будем, т.к. все можно найти в документации, а просто рассмотрим пару вариантов использования и один из основных методов объекта Deffered - $.when(). Изменим наш код выше таким образом:
function ajaxRequest(data) {
return $.ajax({
url: '/handler.php',
type: 'POST',
data: {
upd: data
}
});
}
$('.some-button').on('click', function(){
$.when( ajaxRequest($(this).data('somedata')) ).then(
function (response) {
console.log(response); // данные ответа с сервера
},
function (error) {
console.log(error.statusText); // текст ошибки
}
);
});
Объект с обработчиками нам уже не нужен, т.к. мы обрабатываем ответ сервера на месте. Опции success, error и т.д. - нам так же не нужны, так как метод $.when(), создавая новый deferred-объект, следит за состоянием процесса. А в методе .then(), мы устанавливаем обработчики событий:
- doneCallbacks - функция, которая будет вызвана в случае успешно выполнения
- failCallbacks - функция вызываемая при возникновении ошибки
- И, при желании, progressCallbacks - обработчик события "progress"
Кому-то может показаться такой подход малополезным, но представим ситуацию, когда вам нужно сделать два, три или более ajax-запросов, дождаться завершения каждого, обработать ответы и только потом вывести на экран. Любители "индусского кода" не растеряются :) Они напишут тонну лишнего кода, в то время, как это можно сделать достаточно легко с помощью того же метода $.when():
function ajaxRequest(data) {
return $.ajax({
url: '/handler.php',
type: 'POST',
data: {
upd: data
}
});
}
$('.some-button').on('click', function(){
$.when(
ajaxRequest('somedata1'),
ajaxRequest('somedata2'),
ajaxRequest('somedata3')
).then(
function (r1, r2, r3) {
console.log(r1[0]); // данные ответа первого ajax-запроса
console.log(r2[0]); // второго ajax-запроса
console.log(r3[0]); // третьего ajax-запроса
}
);
});
Функция Ajax осталась одна, ничего лишнего писать не пришлось и результат тот, которого мы ожидали.
И напоследок, покажу, как можно использовать Promise на чистом JavaScript. Сам код Ajax-запроса я описывать тут не буду, т.к. я его уже разбирал в статье Запрос на чистом JavaScript, а схематически это будет выглядеть так:
function ajaxRequest(data) {
return new Promise(function(resolve, reject) {
/* прочий код Ajax-запроса */
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200){
// вызываем callback функцию успешного выполнения,
// передавая полученный ответ
return resolve(xhr.responseText);
}
}
});
}
document.querySelector('.some-button').addEventListener('click', function(){
ajaxRequest('somedata').then(
function(response){
console.log(response); // данные ответа ajax-запроса
}
);
}, false);
Надеюсь, что этот краткий обзор, поможет новичкам сориентироваться в правильном направлении. Более подробно про функции объекта Deferred - читайте в официальной документации ;)