Начнем с объекта Deferred, разобраться с ним и уметь применять – это прям высший пилотаж. Давайте посмотрим, как он работает, откройте нашу песочницу:
Запустите скрипт в консоли:
// инициализация Deferred объекта// статус «ожидает выполнение»varD=$.Deferred();// подключаем обработчикиD.then(function() { console.log("first") });D.then(function() { console.log("second") });// изменяем статус на «fulfilled» - «выполнен успешно»// для этого вызываем resolve()// для наглядности подождём секундочку// наши обработчики будут вызваны в порядке очереди, но они не ждут друг другаsetTimeout(() =>D.resolve(),1000)// данный обработчик подключён слишком поздно и будет вызван сразуD.then(function() { console.log("third") });
Приведенные примеры будут работать на любой странице, где подключен jQuery ;)
Если всё это перевести на человеческий язык, получится следующий сценарий:
если всё будет хорошо, тогда выполни вот эту функцию и выведи «first»
и ещё вот эту функцию — «second»
resolve() — мы узнали, всё хорошо
если всё хорошо, выполняем функцию и выводим «third»
Кроме сценариев с «happy end», есть ещё и грустные истории, когда всё пошло не так, как нам бы хотелось:
// инициализация Deferred объектаvarD=$.Deferred();// подключаем обработчикиD.then(function() { console.info("done") });D.catch(function() { console.error("fail") });// изменяем статус на «rejected» - «выполнен с ошибкой»// для наглядности подождём секундочкуsetTimeout(() =>D.reject(),1000)// в консоли нас будет ожидать лишь «fail» :(
Получается так:
если всё будет хорошо, тогда выполни вот эту функцию — «done»
если всё будет плохо, тогда вот эта функция выведет «fail»
ой, всё плохо
В действительности, метод then() позволяет вешать одновременно как обработчики для положительного сценария, так и для варианта с ошибкой:
Становится ли от этого код читаемым – сомневаюсь, но такой вариант существует и используется повсеместно. Я же предпочитаю для отлова ошибок использовать метод catch(), который по своей сути лишь сокращенная запись для then(null, fn):
varD=$.Deferred();// подключаем обработчик ошибок через then()D.then(null,function() { console.error("fail") });// подключаем обработчик ошибок через catch()D.catch(function() { console.error("again fail") });
Ещё упомяну метод always() – он добавляет обработчики, которые будут выполнены вне зависимости от случившегося (в действительности, внутри происходит вызов done(arguments) и fail(arguments)).
Чтобы не путаться в перечисленных методах, приведу блок-схему:
При вызове resolve() и reject() можно передать произвольные данные в зарегистрированные callback-функции для дальнейшей работы:
Кроме того, существуют ещё методы resolveWith() и rejectWith(), они позволяют изменять контекст вызываемых callback-функций (т.е. внутри них «this» будет смотреть на указанный контекст):
/// инициализация Deferred объектаvarD=$.Deferred();// счётчик электрическийfunctionCounter () {this.counter =0this.tick=function() {this.counter++console.log(`tick: ${this.counter}`) }}var counter =newCounter()// подключаем обработчики// this будет смотреть на наш counterD.then(function() { this.tick() });D.then(function() { this.tick() });// вызываем resolveWith()D.resolveWith(counter)
Отдельно отмечу, что если вы собираетесь передать Deferred объект «на сторону», чтобы «там» могли повесить свои обработчики событий, но не хотите потерять контроль, то возвращайте не сам объект, а результат выполнения метода promise() – фактически это будет искомый объект в режиме «read-only»:
А ещё, кроме поведения «ждём чуда» с помощью Deferred можно выстраивать цепочки вызовов – «живые очереди»:
varD=$.Deferred();D.then(function() {// ждём resolve()console.log("hide images")return$("article img").slideUp(2000).promise()}).then(function(){// подождём, пока спрячутся картинкиconsole.log("hide paragraphs")return$("article p").slideUp(2000).promise()}).then(function(){// подождём, пока спрячутся параграфыconsole.log("hide articles")return$("article").hide(2000).promise()}).then(function(){// всё сделано, шефconsole.info("done")});// секунду и всё будетsetTimeout(() =>D.resolve(),1000)
Подобное поведение мы уже реализовывали используя метод animate(), но нам же хочется найти свой путь:
До jQuery 1.8 тут шла речь о методе pipe(), а теперь о then()
В данном примере мы вызываем метод then(), которому скормлена callback-функция, которая должна возвращать объект Promise. Это необходимо для соблюдения порядка в очереди – попробуйте убрать в примере один «return», и вы заметите, что следующая анимация наступит не дождавшись завершения предыдущей:
varD=$.Deferred();D.then(function() {// ждём resolve()console.log("hide images")$('article img').slideUp(2000).promise()}).then(function(){// подождём, пока спрячутся картинкиconsole.log("hide paragraphs")$('article p').slideUp(2000).promise()}).then(function(){// подождём, пока спрячутся параграфыconsole.log("hide articles")return$('article').hide(2000).promise()}).then(function(){// всё сделано, шефconsole.info("done")});// секунду и всё будетsetTimeout(() =>D.resolve(),1000)
На этом возможности Deferred ещё не завершились. Есть ещё связка методов notify() и progress() – первый шлёт послания в callback-функции, которые зарегистрированы с помощью второго. Приведу наглядный код для демонстрации (откройте консоль и посмотрите, что получается):