Node.js: Когда использовать Promises vs Callbacks

У меня есть старый код Node.js, который я обновляю. В этом процессе я разрабатываю новые модули для работы со старым кодом. Я нахожу это сейчас, в отличие от того, когда я впервые написал это, я больше полагаюсь на использование ES6 promises, а не на обратные вызовы. Итак, теперь у меня есть это сочетание некоторых функций, возвращающих promises, а некоторые - обратные вызовы - это утомительно. Я думаю, что в конечном итоге он должен быть реорганизован для использования promises. Но прежде чем это будет сделано...

Каковы ситуации, когда promises являются предпочтительными и где предпочтительны обратные вызовы?

Есть ли какая-либо ситуация, с которой обратный вызов может справиться лучше, чем обещание и наоборот?

Основываясь на том, что я видел до сих пор, я не вижу причин использовать обратные вызовы вместо promises. Это правда?

Ответ 1

Во-первых, вы почти никогда не хотите писать код, представляющий собой смесь обратных вызовов и обещаний для асинхронных операций. Если вы переходите к обещаниям или вводите некоторые обещания, то, возможно, вы захотите реорганизовать обратные вызовы в том же разделе кода в обещания. Для соответствующих типов операций существует так много преимуществ обещаний по сравнению с простыми обратными вызовами, что стоит потратить усилия на преобразование, когда вы уже работаете в области кода.

Обещания отлично подходят для:

  • Мониторинг синхронных операций
  • Это нужно уведомить только один раз (обычно завершение или ошибка)
  • Координация или управление несколькими асинхронными операциями, такими как секвенирование или ветвление асинхронных операций, или управление несколькими операциями в полете одновременно
  • Распространение ошибок от вложенных или глубоко вложенных асинхронных операций
  • Получение кода, готового для использования async/await (или использование его сейчас с транспилером)
  • Операции, которые соответствуют модели Promise, где есть только три состояния: pending, fulfilled и rejected и где переходы состояний из pending => fulfilled или из pending => rejected могут затем измениться (один односторонний переход).
  • Динамическое связывание или создание цепочки асинхронных операций (например, выполнение этих двух асинхронных операций, проверка результата, а затем решение о том, какие другие асинхронные операции следует выполнить на основе промежуточного результата)
  • Управление сочетанием асинхронных и синхронных операций
  • Автоматически перехватывает и распространяет вверх любые исключения, возникающие в обратных вызовах асинхронного завершения (в простых обратных вызовах эти исключения иногда скрыты).

Простые обратные вызовы хороши для вещей, которые обещания не могут сделать:

  • Синхронные уведомления (такие как обратный вызов для Array.prototype.map())
  • Уведомления, которые могут появляться более одного раза (и, следовательно, необходимо вызывать обратный вызов более одного раза). Обещания являются одноразовыми устройствами и не могут использоваться для повторных уведомлений.
  • Ситуации, которые нельзя отобразить в ожидающую, выполненную, отклоненную модель одностороннего состояния.

И я бы также добавил EventEmitter в микс.

EventEmitters отлично подходят для:

  • Публикация/подписка на уведомления типа
  • Интерфейс с моделью событий, особенно когда события могут происходить более одного раза (например, потоки)
  • Слабые связи, когда сторонний код хочет участвовать или контролировать что-то без API, кроме eventEmitter. Нет API для разработки. Просто сделайте eventEmitter общедоступным и определите некоторые события и данные, которые идут с ними.

Примечания о преобразовании простого кода обратного вызова в Promises

Если ваши обратные вызовы соответствуют соглашению о вызовах узлов с обратным вызовом, переданным в качестве последнего аргумента и вызванным как этот callback(err, result), то вы несколько автоматически util.promisify() родительскую функцию в обещание с помощью util.promisify() в node.js или если используя библиотеку обещаний Bluebird, с Promise.promisify().

С Bluebird вы можете даже пообещать целый модуль (который использует асинхронные обратные вызовы в соглашении о вызовах node.js), например:

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

fs.writeFileAsync("file.txt", data).then(() => {
    // done here
}).catch(err => {
    // error here
});

В версии node.js 8+

Теперь есть util.promisify() который преобразует асинхронную функцию, использующую соглашение об асинхронных вызовах node.js, в функцию, которая возвращает обещание.

Пример из документа:

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);

// usage of promisified function
stat('.').then((stats) => {
  // Do something with 'stats'
}).catch((error) => {
  // Handle the error.
});

Ответ 2

Они оба существуют для решения одной и той же проблемы, обработки результата асинхронной функции.

Обратные вызовы, как правило, более многословны, и одновременная координация нескольких асинхронных запросов может привести к адскому обратному вызову, если вы не активно модулируете свои функции. Обработка и отслеживание ошибок, как правило, менее просты и даже сбивают с толку, поскольку может быть много объектов Error, которые все возвращаются к единой ошибке дальше по стеку вызовов. Ошибки также необходимо передавать обратно исходному вызывающему объекту, что также может привести к некоторому царапанию головы при определении места исходной ошибки, если в цепочке обратного вызова использовались анонимные функции. Одним из преимуществ обратных вызовов является то, что они являются просто старыми функциями и не требуют никакого дополнительного понимания, помимо понимания того, как работает асинхронная операция.

Обещания встречаются чаще, так как для них требуется меньше кода, они более читабельны, поскольку они написаны как синхронные функции, имеют один канал ошибок, могут обрабатывать util.promisify() ошибки и с помощью util.promisify(), добавляемого в последней версии Node.js, могут преобразовать Call-First Callbacks в обещания. Есть также async/await который теперь входит и в Node.js, и они также взаимодействуют с Promises.

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

Ответ 3

Я не помню, откуда я взял этот материал, но мог бы помочь лучше понять обещания.

Обещания не являются обратными вызовами. Обещание представляет будущий результат асинхронной операции. Конечно, записывая их так, как вы делаете, вы получаете небольшую выгоду. Но если вы напишите их так, как они предназначены для использования, вы можете написать асинхронный код способом, похожим на синхронный код, и за ним будет намного легче следовать: ПРЕИМУЩЕСТВА 1. Читаемость по обратным вызовам 2. Легко обнаруживать ошибки. 3. Одновременные обратные вызовы

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

aAsync()   
.then(bAsync)  
 .then(cAsync)   
.done(finish); 

гораздо более читабельным, чем эквивалент передачи каждой из этих отдельных функций в качестве обратных вызовов, как

aAsync(function(){
    return bAsync(function(){
        return cAsync(function(){
            finish()         
        })     
    }) 
}); 

2. Легко ловить ошибки. Конечно, не намного меньше кода, но гораздо более читабельно. Но это не конец. Позвольте узнать истинные преимущества: что если вы хотите проверить наличие ошибок на любом из этапов? Было бы адски делать это с обратными вызовами, но с обещаниями, это кусок пирога:

api()
.then(function(result) {   
    return api2(); 
})
.then(function(result2){     
    return api3(); 
})
.then(function(result3){      
    // do work 
})
.catch(function(error) {    
    //handle any error that may occur before this point 
}); 
/* Pretty much the same as a try { ... } catch block. 
Even better: */
api()
.then(function(result){
    return api2(); })
.then(function(result2){
    return api3(); })
.then(function(result3){
    // do work 
})
.catch(function(error) {
    //handle any error that may occur before this point 
})
.then(function() {
    //do something whether there was an error or not      
    //like hiding an spinner if you were performing an AJAX request. 
});

3. Одновременные обратные вызовы И даже лучше: что, если эти 3 вызова api, api2, api3 могут выполняться одновременно (например, если это были вызовы AJAX), но вам нужно подождать три? Без обещаний вам придется создать какой-то счетчик. С обещаниями, используя обозначение ES6, это еще один кусок пирога и довольно аккуратный:

Promise.all([api(), api2(), api3()])
.then(function(result) {
    //do work. result is an array containing the values of the three fulfilled promises. 
})
.catch(function(error) {
    //handle the error. At least one of the promises rejected. 
});

Надеюсь, вы видите Обещания в новом свете.