Есть ли фундаментальная разница между обратными вызовами и обещаниями?


94

При выполнении однопоточного асинхронного программирования я знаком с двумя основными приемами. Наиболее распространенным является использование обратных вызовов. Это означает передачу функции, которая асинхронно действует как функция обратного вызова в качестве параметра. Когда асинхронная операция завершится, будет вызван обратный вызов.

Некоторый типичный jQueryкод, разработанный таким образом:

$.get('userDetails', {'name': 'joe'}, function(data) {
    $('#userAge').text(data.age);
});

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

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

Разница между Promises и традиционным подходом к обратным вызовам заключается в том, что асинхронные методы теперь синхронно возвращают объекты Promise, для которых клиент устанавливает обратный вызов. Например, аналогичный код с использованием Promises в AngularJS:

$http.get('userDetails', {'name': 'joe'})
    .then(function(response) {
        $('#userAge').text(response.age);
    });

Итак, мой вопрос: есть ли на самом деле реальная разница? Разница кажется чисто синтаксической.

Есть ли более глубокая причина использовать одну технику над другой?


8
Да: обратные вызовы - это просто функции первого класса. Обещания - это монады, которые обеспечивают компоновочный механизм для цепочки операций над значениями, и в них используются функции более высокого порядка с обратными вызовами для обеспечения удобного интерфейса.
amon


5
@gnat: Учитывая относительное качество двух вопросов / ответов, повторное голосование должно быть наоборот ИМХО.
Барт ван Инген Шенау

Ответы:


110

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

Глубокая причина, почему обещания часто лучше, состоит в том, что они более сочетаемы , что примерно означает, что объединение нескольких обещаний «просто работает», а объединение нескольких обратных вызовов часто - нет. Например, тривиально назначить обещание переменной и позже присоединить к нему дополнительные обработчики или даже присоединить обработчик к большой группе обещаний, которая выполняется только после разрешения всех обещаний. Хотя вы можете эмулировать эти вещи с помощью обратных вызовов, для этого требуется гораздо больше кода, его очень сложно сделать правильно, а конечный результат, как правило, гораздо менее обслуживаем.

Один из самых больших (и тончайших) способов получения обещаний их компоновки - это единообразная обработка возвращаемых значений и необработанных исключений. В случае обратных вызовов способ обработки исключения может полностью зависеть от того, какой из множества вложенных обратных вызовов его выбросил, и какая из функций, принимающих обратные вызовы, имеет реализацию try / catch. С обещаниями вы знаете, что исключение, которое выходит за пределы одной функции обратного вызова, будет перехвачено и передано в обработчик ошибок, предоставленный вами .error()или .catch().

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


Вот попытка некоторого гипотетического кода, написанного с обещаниями, а затем с обратными вызовами, которые должны быть достаточно сложными, чтобы дать вам некоторое представление о том, о чем я говорю.

С обещаниями:

createViewFilePage(fileDescriptor) {
    getCurrentUser().then(function(user) {
        return isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id);
    }).then(function(isAuthorized) {
        if(!isAuthorized) {
            throw new Error('User not authorized to view this resource.'); // gets handled by the catch() at the end
        }
        return Promise.all([
            loadUserFile(fileDescriptor.id),
            getFileDownloadCount(fileDescriptor.id),
            getCommentsOnFile(fileDescriptor.id),
        ]);
    }).then(function(fileData) {
        var fileContents = fileData[0];
        var fileDownloads = fileData[1];
        var fileComments = fileData[2];
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }).catch(showAndLogErrorMessage);
}

С обратными вызовами:

createViewFilePage(fileDescriptor) {
    setupWidgets(fileContents, fileDownloads, fileComments) {
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }

    getCurrentUser(function(error, user) {
        if(error) { showAndLogErrorMessage(error); return; }
        isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id, function(error, isAuthorized) {
            if(error) { showAndLogErrorMessage(error); return; }
            if(!isAuthorized) {
                throw new Error('User not authorized to view this resource.'); // gets silently ignored, maybe?
            }

            var fileContents, fileDownloads, fileComments;
            loadUserFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileContents = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getFileDownloadCount(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileDownloads = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getCommentsOnFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileComments = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
        });
    });
}

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


1
Еще одним важным преимуществом обещаний является то, что они поддаются дальнейшей «сахаризации» с помощью async / await или сопрограммы, которая возвращает обещанные значения для yieldобещаний ed. Преимущество здесь в том, что вы получаете возможность смешивать в собственных структурах потока управления, которые могут варьироваться в зависимости от того, сколько асинхронных операций они выполняют. Я добавлю версию, которая показывает это.
Acjay

9
Принципиальная разница между обратными вызовами и обещаниями заключается в инверсии контроля. С обратными вызовами ваш API должен принимать обратный вызов , но с обещаниями ваш API должен обеспечивать обещание . Это основное отличие, и оно имеет широкие последствия для разработки API.
cwharris

@ChristopherHarris не уверен, что я бы согласился. наличие then(callback)метода в Promise, который принимает обратный вызов (вместо метода в API, принимающего этот обратный вызов), не должен ничего делать с IoC. Promise вводит один уровень косвенности, который полезен для компоновки, объединения в цепочку и обработки ошибок (в действительности это ориентированное на железную дорогу программирование), но клиент по-прежнему не выполняет обратный вызов, поэтому на самом деле отсутствие IoC.
dragan.stepanovic

1
@ dragan.stepanovic Вы правы, и я использовал неправильную терминологию. Разница заключается в косвенности. С обратным вызовом вы уже должны знать, что нужно сделать с результатом. С обещанием вы можете решить позже.
cwharris
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.