Долгое время разработчики JavaScript использовали функции обратного вызова для выполнения нескольких задач. Очень распространенным примером является добавление обратного вызова через функцию addEventListener()
для выполнения различных операций при возникновении события, такого как click
или keypress
. Функции обратного вызова просты и выполняют работу для простых случаев. К сожалению, когда ваши веб-страницы усложняются и вам нужно выполнять много асинхронных операций, либо параллельно, либо последовательно, они становятся неуправляемыми.
ECMAScript 2015 (также известный как ECMAScript 6) представил нативное средство для решения таких ситуаций: обещания. Если вы не знаете, что такое обещания, вы можете прочитать статью Обзор обещаний JavaScript . jQuery предоставил и по-прежнему предоставляет свой собственный вариант обещаний, называемых отложенными объектами . Они были введены в jQuery за годы до того, как в ECMAScript были введены обещания. В этой статье я расскажу, что такое Deferred
объекты и какие проблемы они пытаются решить.
Краткая история
Deferred
объект был представлен в jQuery 1.5 как цепная утилита, используемая для регистрации нескольких обратных вызовов в очередях обратного вызова, вызова очередей обратного вызова и передачи состояния успеха или сбоя любой синхронной или асинхронной функции. С тех пор это было предметом обсуждения, некоторой критики и множества изменений на этом пути. Несколько примеров критики: « Вы упускаете суть обещаний» и « Обещания JavaScript» и почему реализация jQuery не работает .
Вместе с объектом Promise Deferred
представляет реализацию обещаний в jQuery. В jQuery версии 1.x и 2.x Deferred
объект придерживается предложения CommonJS Promises / A. Это предложение использовалось в качестве основы для предложения Promises / A +, на котором основаны нативные обещания. Как упоминалось во введении, причина, по которой jQuery не придерживается предложения Promises / A +, заключается в том, что он реализовал обещания еще до того, как это предложение было задумано.
Поскольку jQuery был предшественником и из-за проблем обратной совместимости, существуют различия в том, как вы можете использовать обещания в чистом JavaScript и в jQuery 1.x и 2.x. Более того, поскольку jQuery следует другому предложению, библиотека несовместима с другими библиотеками, в которых реализованы обещания, такими как библиотека Q.
В следующем выпуске jQuery 3 улучшена совместимость с собственными обещаниями (как реализовано в ECMAScript 2015). Сигнатура основного метода ( then()
) все еще немного отличается по причинам обратной совместимости, но поведение в большей степени соответствует стандарту.
Обратные вызовы в jQuery
Чтобы понять, почему вам может понадобиться использовать Deferred
объект, давайте обсудим пример. При использовании jQuery очень часто используются его методы Ajax для выполнения асинхронных запросов. В качестве примера предположим, что вы разрабатываете веб-страницу, которая отправляет запросы Ajax в API GitHub. Ваша цель — извлечь список репозиториев пользователя, найти последний обновленный репозиторий, найти первый файл со строкой «README.md» в его имени и, наконец, получить содержимое этого файла. Основываясь на этом описании, каждый Ajax-запрос может начинаться только после завершения предыдущего шага. Другими словами, запросы должны выполняться последовательно .
Превратив это описание в псевдокод (обратите внимание, что я не использую настоящий GitHub API), мы получаем:
var username = 'testuser'; var fileToSearch = 'README.md'; $.getJSON('https://api.github.com/user/' + username + '/repositories', function(repositories) { var lastUpdatedRepository = repositories[0].name; $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files', function(files) { var README = null; for (var i = 0; i < files.length; i++) { if (files[i].name.indexOf(fileToSearch) >= 0) { README = files[i].path; break; } } $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content', function(content) { console.log('The content of the file is: ' + content); }); }); });
Как вы можете видеть в этом примере, используя обратные вызовы, мы должны вкладывать вызовы для выполнения запросов Ajax в нужной нам последовательности. Это делает код менее читабельным. Ситуацию, когда у вас много вложенных обратных вызовов или независимых обратных вызовов, которые необходимо синхронизировать, часто называют «адом обратного вызова».
Чтобы сделать его немного лучше, вы можете извлечь именованные функции из анонимных встроенных функций, которые я создал. Однако, это изменение не очень помогает, и мы все еще находимся в аду обратного вызова. Введите Deferred
объекты и объекты Promise
.
Отложенные и обещанные объекты
Deferred
объект может использоваться при выполнении асинхронных операций, таких как запросы Ajax и анимации. В jQuery объект Promise
создается из Deferred
объекта или объекта jQuery
. Он обладает подмножеством методов объекта Deferred
: always()
, done()
, fail()
, state()
, а then()
. Я расскажу об этих и других методах в следующем разделе.
Если вы пришли из родного мира JavaScript, вы можете быть смущены существованием этих двух объектов. Зачем иметь два объекта ( Deferred
и Promise
), если в JavaScript есть один ( Promise
)? Чтобы объяснить разницу и варианты их использования, я приму аналогию, которую я использовал в своей книге jQuery in Action, третье издание .
Deferred
объекты обычно используются, если вы пишете функцию, которая имеет дело с асинхронными операциями и которая должна возвращать значение (которое также может быть ошибкой или вообще не иметь значения). В этом случае ваша функция является источником значения, и вы хотите запретить пользователям изменять состояние Deferred
. Объект обещания используется, когда вы являетесь пользователем функции.
Чтобы прояснить эту концепцию, предположим, что вы хотите реализовать функцию timeout()
основанную на обещаниях (я покажу вам код для этого примера в следующем разделе этой статьи ). Вы — тот, кто отвечает за написание функции, которая должна ждать определенное количество времени (в этом случае значение не возвращается). Это делает тебя продюсером . Потребитель вашей функции не заботится о ее разрешении или отклонении. Потребитель должен иметь возможность добавлять функции для выполнения только после выполнения, сбоя или выполнения Deferred
. Кроме того, вы хотите убедиться, что потребитель не может разрешить или отклонить Deferred
по своему усмотрению. Для достижения этой цели вам необходимо вернуть объект Promise
Deferred
, созданного вами в функции timeout()
, а не сам Deferred
. Тем самым вы гарантируете, что никто не сможет вызвать метод resolve()
или reject()
кроме вашей функции timeout()
.
Вы можете узнать больше о разнице между объектами jQuery Deferred и Promise в этом вопросе StackOverflow .
Теперь, когда вы знаете, что это за объекты, давайте взглянем на доступные методы.
Отложенные методы
Deferred
объект достаточно гибок и предоставляет методы для всех ваших потребностей. Его можно создать, вызвав метод jQuery.Deferred()
следующим образом:
var deferred = jQuery.Deferred();
или, используя ярлык $
:
var deferred = $.Deferred();
После создания Deferred
объект предоставляет несколько методов. Игнорирование тех, кто устарел или удален, они:
-
always(callbacks[, callbacks, ..., callbacks])
: добавить обработчики, которые будут вызываться, когдаDeferred
объект либо разрешен, либо отклонен. -
done(callbacks[, callbacks, ..., callbacks])
: добавить обработчики, которые будут вызываться при разрешенииDeferred
объекта. -
fail(callbacks[, callbacks, ..., callbacks])
: добавить обработчики, которые будут вызываться при отклоненииDeferred
объекта. -
notify([argument, ..., argument])
: вызыватьprogressCallbacks
дляDeferred
объекта с заданными аргументами. -
notifyWith(context[, argument, ..., argument])
:notifyWith(context[, argument, ..., argument])
progressCallbacks
дляDeferred
объекта с заданным контекстом и аргументами. -
progress(callbacks[, callbacks, ..., callbacks])
: добавить обработчики, которые будут вызываться, когдаDeferred
объект генерирует уведомления о прогрессе. -
promise([target])
: вернуть объектPromise
Deferred
. -
reject([argument, ..., argument])
: отклонитьDeferred
объект и вызвать любыеfailCallbacks
с заданными аргументами. -
rejectWith(context[, argument, ..., argument])
: отклонитьDeferred
объект и вызвать любыеfailCallbacks
с заданным контекстом и аргументами. -
resolve([argument, ..., argument])
:resolve([argument, ..., argument])
Deferred
объект и вызвать любыеdoneCallbacks
с заданными аргументами. -
resolveWith(context[, argument, ..., argument])
: разрешитьDeferred
объект и вызвать любыеdoneCallbacks
с заданным контекстом и аргументами. -
state()
: определить текущее состояниеDeferred
объекта. -
then(resolvedCallback[, rejectedCallback[, progressCallback]])
: добавить обработчики, которые будут вызываться, когдаDeferred
объект разрешен, отклонен или все еще выполняется.
Описание этих методов дает мне возможность выделить одно различие между терминологией, используемой в документации jQuery, и спецификациями ECMAScript. В спецификациях ECMAScript обещание считается выполненным, когда оно выполнено или отклонено. Однако в документации jQuery слово resolved используется для обозначения того, что спецификация ECMAScript называет выполненным состоянием.
Из-за большого количества предоставленных методов невозможно охватить все из них в этой статье. Однако в следующих разделах я покажу вам пару примеров использования Deferred
и Promise
. В первом примере мы перепишем фрагмент, рассмотренный в разделе «Обратные вызовы в jQuery», но вместо использования обратных вызовов мы будем использовать эти объекты. Во втором примере я поясню обсуждаемую аналогию между производителем и потребителем.
Ajax-запросы в последовательности с отложенным
В этом разделе я покажу, как использовать объект Deferred
и некоторые его методы для улучшения читабельности кода, разработанного в разделе «Обратные вызовы в jQuery». Прежде чем углубляться в это, мы должны понять, какой из доступных методов нам нужен.
В соответствии с нашими требованиями и списком предоставленных методов ясно, что мы можем использовать метод done()
или then()
для управления успешными случаями. Поскольку многие из вас, возможно, уже привыкли к объекту JavaScript Promise
, в этом примере я буду использовать метод then()
. Одно из важных различий между этими двумя методами заключается в том, что then()
может переадресовывать полученное значение в качестве параметра другим вызовам then()
, done()
, fail()
или progress()
определенных после него.
Окончательный результат показан ниже:
var username = 'testuser'; var fileToSearch = 'README.md'; $.getJSON('https://api.github.com/user/' + username + '/repositories') .then(function(repositories) { return repositories[0].name; }) .then(function(lastUpdatedRepository) { return $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/files'); }) .then(function(files) { var README = null; for (var i = 0; i < files.length; i++) { if (files[i].name.indexOf(fileToSearch) >= 0) { README = files[i].path; break; } } return README; }) .then(function(README) { return $.getJSON('https://api.github.com/user/' + username + '/repository/' + lastUpdatedRepository + '/file/' + README + '/content'); }) .then(function(content) { console.log(content); });
Как видите, код гораздо удобнее для чтения, поскольку мы можем разбить весь процесс на маленькие шаги, которые находятся на одном уровне (в отношении отступов).
Создание основанной на обещаниях функции setTimeout
Как вы, возможно, знаете, setTimeout () — это функция, которая выполняет функцию обратного вызова через определенный промежуток времени. Оба эти элемента (функция обратного вызова и время) должны быть представлены в качестве аргументов. Допустим, вы хотите записать сообщение на консоль через одну секунду. Используя функцию setTimeout()
, вы можете достичь этой цели с помощью кода, показанного ниже:
setTimeout( function() { console.log('I waited for 1 second!'); }, 1000 );
Как видите, первый аргумент — это функция, которую нужно выполнить, а второй — количество миллисекунд ожидания. Эта функция хорошо работала в течение многих лет, но что, если вам нужно ввести задержку в цепочке Deferred
платежей?
В следующем коде я покажу вам, как использовать объект Promise
, предоставляемый jQuery, для разработки основанной на обещаниях функции setTimeout()
. Для этого я буду использовать метод promise()
Deferred
объекта.
Окончательный результат показан ниже:
function timeout(milliseconds) { // Create a new Deferred object var deferred = $.Deferred(); // Resolve the Deferred after the amount of time specified by milliseconds setTimeout(deferred.resolve, milliseconds); // Return the Deferred's Promise object return deferred.promise(); } timeout(1000).then(function() { console.log('I waited for 1 second!'); });
В этом листинге я определил функцию timeout()
которая оборачивает встроенную в JavaScript функцию setTimeout()
. Внутри timeout()
я создал новый Deferred
объект для управления асинхронной задачей, состоящей из разрешения Deferred
объекта через указанное количество миллисекунд. В этом случае функция timeout()
является источником значения, поэтому она создает Deferred
объект и возвращает объект Promise
. Тем самым я гарантирую, что вызывающая функция (потребитель) не сможет разрешить или отклонить Deferred
объект по своему желанию. Фактически, вызывающая сторона может только добавлять функции для выполнения, используя такие методы, как done()
и fail()
.
Различия между jQuery 1.x / 2.x и jQuery 3
В первом примере с использованием Deferred
мы разработали фрагмент, который ищет файл, содержащий в своем имени строку «README.md», но мы не учли ситуацию, в которой такой файл не найден. Эта ситуация может рассматриваться как провал. Когда это происходит, мы можем разорвать цепочку вызовов и перейти прямо к ее концу. Для этого было бы естественно создать исключение и перехватить его с помощью метода fail()
, как вы это сделали бы с помощью метода catch()
JavaScript.
В библиотеках Promises / A и Promises / A + (например, jQuery 3.x) выброшенное исключение преобразуется в отклонение и вызывается обратный вызов сбоя, например, добавленный с помощью fail()
. Это получает исключение в качестве аргумента.
В jQuery 1.x и 2.x необработанное исключение остановит выполнение программы. Эти версии позволяют всплывающему исключению всплывать, обычно достигая window.onerror
. Если не определена функция для обработки этого исключения, отображается сообщение об исключении, и выполнение программы прерывается.
Чтобы лучше понять различное поведение, взгляните на этот пример из моей книги:
var deferred = $.Deferred(); deferred .then(function() { throw new Error('An error message'); }) .then( function() { console.log('First success function'); }, function() { console.log('First failure function'); } ) .then( function() { console.log('Second success function'); }, function() { console.log('Second failure function'); } ); deferred.resolve();
В jQuery 3.x этот код записывает сообщения «Первая функция отказа» и «Вторая функция успеха» на консоль. Причина в том, что, как я упоминал ранее, в спецификации говорится, что выброшенное исключение должно быть преобразовано в отклонение, а обратный вызов сбоя должен быть вызван с исключением. Кроме того, как только исключение было обработано (в нашем примере обратным вызовом сбоя, переданным секунде then()
), должны быть выполнены следующие функции успеха (в этом случае обратный вызов успеха передан третьему then()
).
В jQuery 1.x и 2.x не выполняется ни одна, кроме первой функции (вызывающей ошибку), и на консоли будет отображаться только сообщение «Uncaught Error: сообщение об ошибке».
JQuery 1.x / 2.x
JQuery 3
Для дальнейшего улучшения совместимости с ECMAScript 2015 jQuery 3 также добавляет новый метод к объектам Deferred
и Promise
называемый catch()
. Это метод для определения обработчика, выполняемого, когда Deferred
объект rejected
или его объект Promise
находится в отклоненном состоянии. Его подпись выглядит следующим образом:
deferred.catch(rejectedCallback)
Этот метод — всего лишь ярлык для then(null, rejectedCallback)
.
Выводы
В этой статье я познакомил вас с выполнением обещаний в jQuery. Обещания позволяют избежать неприятных уловок для синхронизации параллельных асинхронных функций и необходимости вкладывать обратные вызовы внутри обратных вызовов внутри обратных вызовов…
Помимо нескольких примеров, я также рассказал о том, как jQuery 3 улучшает взаимодействие с собственными обещаниями. Несмотря на различия, отмеченные между старыми версиями jQuery и ECMAScript 2015, Deferred
остается невероятно мощным инструментом, который есть в вашем наборе инструментов. Как профессиональный разработчик и с возрастающей сложностью ваших проектов, вы обнаружите, что используете его много.