Чи дійсно є принципова різниця між зворотними дзвінками та обіцянками?


94

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

Деякі типові jQueryкоди розроблені таким чином:

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

Однак цей тип коду може стати брудним і сильно вкладеним, коли ми хочемо робити додаткові виклики асинхронізації один за одним, коли попередній закінчується.

Отже, другий підхід - це використання Обіцянок. Обіцянка - це об'єкт, який представляє значення, яке може ще не існувати. Ви можете встановити на ньому зворотні дзвінки, які будуть викликатись, коли значення буде готове прочитати.

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

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

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

Чи є якась глибша причина використовувати одну техніку над іншою?


8
Так: зворотні виклики - це лише першокласні функції. Обіцяння - це монади, які надають складовий механізм для ланцюгових операцій над значеннями, і, можливо, використовують функції вищого порядку з зворотними зворотами, щоб забезпечити зручний інтерфейс.
амон


5
@gnat: Враховуючи відносну якість двох питань / відповідей, повторне голосування повинно бути навпаки IMHO.
Барт ван Інген Шенау

Відповіді:


110

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

Найглибшою причиною, чому обіцянки часто є кращими, є те, що вони більш композиційні , а це означає, що поєднання декількох обіцянок "просто працює", а комбінування декількох зворотних викликів часто не дає змоги. Наприклад, тривіально призначити обіцянку змінній і приєднати до неї додаткові обробники або навіть приєднати обробник до великої групи обіцянок, яка виконується лише після того, як всі обіцянки будуть вирішені. Хоча ви можете впорядкувати ці речі за допомогою зворотних викликів, це потребує набагато більше коду, це дуже важко зробити правильно, а кінцевий результат, як правило, набагато менш досяжний.

Один з найбільших (і найтонших) способів обіцянок отримати їхню комбінованість - це рівномірне поводження зі зворотними значеннями та неприховані винятки. Що стосується зворотних викликів, то, як обробляється виняток, може повністю залежати від того, хто з багатьох вкладених зворотних викликів викинув його, і яка з функцій, що приймають зворотні виклики, має спробувати / уловлювати в своїй реалізації. З обіцянками ви знаєте, що виняток, який уникає однієї функції зворотного виклику, буде зафіксований та переданий до обробника помилок, який ви надали .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
Інша основна перевага обіцянок полягає в тому, що вони піддаються подальшому «цукрозакорененню» за допомогою асинхронізації / очікування або кореневої програми, яка передає обіцяні значення для yieldвиданих обіцянок. Перевага тут полягає в тому, що ви отримуєте можливість змішуватися в нативних структурах потоків управління, які можуть відрізнятися залежно від кількості операцій з асинхронізацією. Додам версію, яка це показує.
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.