Одна функция для Ajax-запросов.

Как вернуть данные.

Долго сидел и думал, какое же дать название этой статье, т.к. тема гораздо обширнее, чем просто "вернуть ответ из функции 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" ( GIF-прелоадер ) перед отправкой запроса или вместо 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(), мы устанавливаем обработчики событий:

  1. doneCallbacks - функция, которая будет вызвана в случае успешно выполнения
  2. failCallbacks - функция вызываемая при возникновении ошибки
  3. И, при желании, 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 - читайте в официальной документации ;)

Incode Pro logo

11 комментариев

sash 21.04.2015 03:06
Братва согласно закивала :)
Как-то и в голову не приходило оборачивать аякс запросы в функции, всё как-то по индусски) А when и then - интересные ребята вроде. Знакомство поставлено в очередь..
sash 06.06.2015 10:03
function _ajax_ (url_, type_, data_, id_){
     $.ajax({
         url : url_,
         type: type_, 
         data: data_,
         success: function(response){
    	    $(id_).html(response);
         }
    });
}

Буду доделывать, как на первый взляд? Я жутко путаюсь в твоих
responseHandler
:)
Гость 06.06.2015 11:56
Спасибо друзяка, отличная статья!

Даже не знал что в jQuery есть promise o_0 хотя успешно его узаю
Посмотри, тут есть "очепятка"

$.when(
ajaxRequest('somedata1'),
ajaxRequest('somedata2'),
ajaxRequest('somedata3')
).then(
function (r1, r2, r3) {
console.log(r1[0]); // данные ответа первого ajax-запроса
console.log(r1[0]); // второго ajax-запроса
console.log(r1[0]); // третьего ajax-запроса
}
);
Incode 14.06.2015 18:23
Посмотри, тут есть "очепятка"
Спасибо, поправил ;)
Гость 05.10.2015 18:51
как в ответе response вытащить наружу? я имею ввиду отсюда:

function (response) {
      console.log(response); // данные ответа с сервера
/* отсюда */
    }

сюда:

$('.some-button').on('click', function(){ /* сюда */ });

я пытался создать глобальную переменную в родительской функции, и присвоить ей ответ сервера, но не присваивается. В тупую игнорится... Я убедился что запрос действительно обрабатывается до конца и только потом идет отработка функции ответа.. но почему же она не выдает мне данные наружу, они ведь уже пришли и ждут?
Incode 05.10.2015 19:14
как в ответе response вытащить наружу?
А разве предпоследний пример (с несколькими запросами) это не демонстрирует. По нажатию на кнопку выполняется запрос или несколько запросов и там же получаете ответ, который уже обрабатываете по своему усмотрению. А создание глобальной переменной ничего не решает, т.к. до выполнения запроса она была не определена или имела какое-то другое значение и изменится только после завершения запроса. Вот пример в песочнице. При первом клике, выведется "old value", т.к. запрос еще не завершился, а вывод уже отрабатывает. При повторном клике, вы уже увидите "new value" - это значение, которое было присвоено после предыдущего ajax-запроса и так далее при последующих кликах. Надеюсь, что так будет более понятно.
Гость 06.09.2016 12:01
Добрый день!
Помогите разобраться, что делаю не так. Вот код:
<button class="update-something" data-action="updateContent" data-id="1">Обновить контент</button>

$(document).ready(function() {
 var valute;
 function ajaxRequest(data) {
     return $.ajax({
     url:  "https://bank.gov.ua/NBUStatService/v1/statdirectory/exchange?json",
     type: 'POST',
     dataType: 'jsonp',
     xhrFields: {
	withCredentials: true,
	headers: {}
     },
     contentType: 'application/json; charset=utf-8'
});
};
$('.update-something').on('click', function(){
	$.when( ajaxRequest()).then(
		function (response) {
		  alert('true');
     	  valute = response;
	}
);
});
				
});

Как уже не пробывал все равно возвращает false.
ответ с сервера приходит код 200, в консоле просматриваю ответ сервера, массив jsonp есть, а работать с ним не могу.
Спасибо за помощь!!!!
Incode 06.09.2016 21:35
Как уже не пробывал все равно возвращает false.
Боюсь, что тут без вариантов. У них в API не предусмотрены запросы с клиента. JSONP предполагает ответ в следующем формате:
callbackFunction({"some_key_1": "some_val_1","some_key_2": "some_val_2"});
То есть, JSON-строка обвернутая в функцию. А в вашем случае, возвращается JSON в чистом виде. Делайте AJAX-запрос к себе на сервер, там средствами PHP (или на том ЯП, который используете) получаете данные из API (даже обычной функцией file_get_contents()) и возвращаете эти данные обратно на клиент. Так работать будет без проблем. Только не забудьте, что в этом случае, нужно тип ожидаемых данных изменить на "json" (dataType: 'json').
Гость 07.09.2016 10:37
Спасибо!!! Огромное спасибо!!!
bazilio100lei 11.04.2017 13:24
Хорошо, но опять же как вернуть за пределы функции $.when().then() результат выполнения?
У меня задача вывести popover на bootstrap с ajax-данными от сервера.

    $('.like__counter_popover').popover({
        content : function() {  
            return result; // сюда нужно вернуть result.output из $.ajax
        },
        html : true,
        placement : 'top'
    });

Как это сделать? Куда вставлять $.ajax и $.when().then() ? Пробовал по-разному, не выходит.
Гость 02.12.2017 06:38
Все примеры (и этот - не исключение) показывают работу с заранее определённым конечным числом запросов:
$.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-запроса
    }
  );

Когда число возможных запросов заранее неизвестно - можно как-то внутри when получить данные последнего из них?
Ваш комментарий:
X