Проблемы, присущие jQuery $.Deferred(jQuery 1.x/2.x)

@Domenic содержит очень подробную статью о недостатках отложенных объектов jQuery: Вам не хватает точки Promises. В нем Доменик выделяет несколько недостатков jQuery promises по сравнению с другими, включая Q, когда .js, RSVP.js и ES6 promises.

Я ухожу от статьи Доменика, чувствуя, что jQuery promises имеет свойственный отказ, концептуально. Я пытаюсь привести примеры к концепции.

Я понимаю, что в реализации jQuery есть две проблемы:

1. Метод .then не связан с цепью

Другими словами

promise.then(a).then(b)

jQuery вызовет a, затем b, когда выполняется promise.

Так как .then возвращает новое обещание в других библиотеках обещаний, их эквивалент будет:

promise.then(a)
promise.then(b)

2. Обработка исключений в jQuery разворачивается.

Другой проблемой будет обработка исключений, а именно:

try {
  promise.then(a)
} catch (e) {
}

Эквивалент в Q будет:

try {
  promise.then(a).done()
} catch (e) {
   // .done() re-throws any exceptions from a
}

В jQuery генерируются исключения и пузырьки, когда a не удается блокировать catch. В другом promises любое исключение в a переносится на .done или .catch или другой асинхронный catch. Если ни один из вызовов API обещаний не поймает исключение, оно исчезает (следовательно, наилучшая практика Q, например, используя .done для освобождения любых необработанных исключений).

 

Устранены ли проблемы, связанные с реализацией jQuery promises, или я неправильно понял или пропустил проблемы?


Изменить. Этот вопрос относится к jQuery < 3,0; с jQuery 3.0 alpha jQuery - это Promises/A +.

Ответ 1

Обновление: jQuery 3.0 исправил проблемы, описанные ниже. Это действительно соответствует Promises/A +.

Да, jQuery promises имеют серьезные и неотъемлемые проблемы.

Тем не менее, поскольку статья была написана, jQuery приложил значительные усилия, чтобы быть более жалобными Promises/Aplus, и теперь у них есть метод .then, который цепочки.

Таким образом, даже в jQuery returnsPromise().then(a).then(b) для функций возврата обещание a и b будет работать, как ожидалось, разворачивая возвращаемое значение перед продолжением вперед. Как показано на рисунке fiddle:

function timeout(){
    var d = $.Deferred();
    setTimeout(function(){ d.resolve(); },1000);
    return d.promise();
}

timeout().then(function(){
   document.body.innerHTML = "First";
   return timeout();
}).then(function(){
   document.body.innerHTML += "<br />Second";
   return timeout();
}).then(function(){
   document.body.innerHTML += "<br />Third";
   return timeout();
});

Однако две огромные проблемы с jQuery - это обработка ошибок и неожиданный порядок выполнения.

Обработка ошибок

Невозможно отметить обещание jQuery, которое отклонено как "обработанное", даже если вы разрешите его, в отличие от catch. Это делает отклонения в jQuery по своей сути сломанными и очень сложными в использовании, не что иное, как синхронный try/catch.

Можете ли вы догадаться, какие журналы здесь? (fiddle)

timeout().then(function(){
   throw new Error("Boo");
}).then(function(){
   console.log("Hello World");
},function(){
    console.log("In Error Handler");   
}).then(function(){
   console.log("This should have run");
}).fail(function(){
   console.log("But this does instead"); 
});

Если вы догадались "uncaught Error: boo", вы были правы. jQuery promises не выбрасывать безопасно. Они не позволят вам обрабатывать любые брошенные ошибки, в отличие от Promises/Aplus promises. Как насчет отказа от безопасности? (fiddle)

timeout().then(function(){
   var d = $.Deferred(); d.reject();
   return d;
}).then(function(){
   console.log("Hello World");
},function(){
    console.log("In Error Handler");   
}).then(function(){
   console.log("This should have run");
}).fail(function(){
   console.log("But this does instead"); 
});

Следующие журналы "In Error Handler" "But this does instead" - вообще не существует способа обращения с отказом обещания jQuery. Это не похоже на поток, который вы ожидаете:

try{
   throw new Error("Hello World");
} catch(e){
   console.log("In Error handler");
}
console.log("This should have run");

Каков поток, который вы получаете с помощью библиотек Promises/A +, таких как Bluebird и Q, и того, что вы ожидаете от полезности. Это огромно, и бросить безопасность - это большой пункт продажи для promises. Вот Bluebird, действующий правильно в этом случае.

Порядок выполнения

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

Если мы посмотрим на следующий код: (fiddle)

function timeout(){
    var d = $.Deferred();
    setTimeout(function(){ d.resolve(); },1000);
    return d.promise();
}
console.log("This");
var p = timeout();
p.then(function(){
   console.log("expected from an async api.");
});
console.log("is");

setTimeout(function(){
    console.log("He");
    p.then(function(){
        console.log("̟̺̜̙͉Z̤̲̙̙͎̥̝A͎̣͔̙͘L̥̻̗̳̻̳̳͢G͉̖̯͓̞̩̦O̹̹̺!̙͈͎̞̬ *");
    });
    console.log("Comes");
},2000);

Мы можем наблюдать, что oh настолько опасное поведение, setTimeout ждет завершения первоначального таймаута, поэтому jQuery переключает свой порядок выполнения, потому что... кому нравятся детерминированные API, которые не вызывают переполнение стека? Вот почему спецификация Promises/A + требует, чтобы promises всегда откладывались до следующего выполнения цикла событий.

Боковое примечание

Стоит упомянуть, что новые и более сильные библиотеки обещаний, такие как Bluebird (и экспериментально When), не требуют .done в конце цепочки, как Q, поскольку они сами определяют необработанные отказы, они также намного быстрее, чем jQuery promises или Q promises.