Existe-t-il une différence fondamentale entre les rappels et les promesses?

94

Lorsque je fais de la programmation asynchrone à un seul thread, je connais bien deux techniques principales. La plus courante consiste à utiliser des rappels. Cela signifie qu'il faut transmettre à la fonction qui agit de manière asynchrone une fonction de rappel en tant que paramètre. Lorsque l'opération asynchrone se termine, le rappel est appelé.

Certains jQuerycodes typiques conçus de cette façon:

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

Cependant, ce type de code peut devenir très confus et hautement imbriqué lorsque nous souhaitons effectuer des appels asynchrones supplémentaires les uns après les autres, une fois l’opération terminée.

Une deuxième approche consiste donc à utiliser les promesses. Une promesse est un objet qui représente une valeur qui pourrait ne pas encore exister. Vous pouvez y définir des rappels, qui seront appelés lorsque la valeur sera prête à être lue.

La différence entre Promises et l'approche traditionnelle des rappels réside dans le fait que les méthodes asynchrones renvoient désormais de manière synchrone les objets Promise, pour lesquels le client définit un rappel. Par exemple, un code similaire utilisant Promises dans AngularJS:

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

Ma question est donc la suivante: existe-t-il réellement une différence? La différence semble être purement syntaxique.

Existe-t-il une raison plus profonde d’utiliser une technique plutôt que l’autre?

Aviv Cohn
la source
8
Oui: les rappels ne sont que des fonctions de première classe. Les promesses sont des monades qui fournissent un mécanisme composable pour enchaîner des opérations sur des valeurs et utilisent des fonctions d'ordre supérieur avec des rappels pour fournir une interface pratique.
amon
5
@gnat: Compte tenu de la qualité relative des deux questions / réponses, le vote en double devrait être l'inverse de l'IMHO.
Bart van Ingen Schenau

Réponses:

110

Il est juste de dire que les promesses ne sont que du sucre syntaxique. Tout ce que vous pouvez faire avec des promesses, vous pouvez le faire avec des rappels. En fait, les implémentations les plus prometteuses vous permettent de convertir les deux à tout moment.

La principale raison pour laquelle les promesses sont souvent meilleures est qu'elles sont plus composables , ce qui signifie approximativement que combiner plusieurs promesses "ne fonctionne pas", alors que combiner plusieurs rappels ne le fait souvent pas. Par exemple, il est simple d'affecter une promesse à une variable et d'y attacher des gestionnaires supplémentaires ultérieurement, ou même d'attacher un gestionnaire à un grand groupe de promesses qui ne sera exécutée qu'une fois toutes les promesses résolues. Bien que vous puissiez en quelque sorte émuler ces rappels, il faut beaucoup plus de code, il est très difficile de le faire correctement et le résultat final est généralement beaucoup moins gérable.

L'un des moyens les plus importants (et les plus subtils) d'obtenir une composition est la gestion uniforme des valeurs de retour et les exceptions non détectées. Avec les rappels, la manière dont une exception est gérée peut dépendre entièrement du choix de nombreux rappels imbriqués, et de la fonction qui prend des rappels dans sa mise en œuvre. Avec des promesses, vous savez qu’une exception qui échappe à une fonction de rappel sera interceptée et transmise au gestionnaire d’erreur que vous avez fourni avec .error()ou .catch().

Pour l'exemple que vous avez donné d'un simple rappel par rapport à une seule promesse, il est vrai qu'il n'y a pas de différence significative. C'est quand vous avez un million de rappels contre un million de promesses que le code basé sur les promesses a tendance à être beaucoup plus agréable.


Voici une tentative de code hypothétique écrit avec des promesses, suivi de rappels qui devraient être suffisamment complexes pour vous donner une idée de ce dont je parle.

Avec des promesses:

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);
}

Avec rappels:

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);
                }
            });
        });
    });
}

Même sans promesse, il existe peut-être des solutions astucieuses pour réduire la duplication de code dans la version des callbacks, mais toutes celles auxquelles je peux penser se résument à mettre en œuvre quelque chose de très prometteur.

Ixrec
la source
1
Un autre avantage majeur des promesses réside dans le fait qu’elles peuvent être soumises à une "sucre" supplémentaire avec async / wait ou à une liste récapitulative qui transmet les valeurs promises pour les yieldpromesses éd. L’avantage ici est que vous avez la possibilité de mélanger des structures de flux de contrôles natifs, dont le nombre d’opérations asynchrones peut varier. Je vais ajouter une version qui montre cela.
mardi
9
La différence fondamentale entre les rappels et les promesses est l'inversion du contrôle. Avec les rappels, votre API doit accepter un rappel , mais avec Promises, elle doit fournir une promesse . C'est la principale différence et elle a de vastes implications pour la conception des API.
cwharris
@ChristopherHarris pas sûr que je suis d'accord. Avoir une then(callback)méthode sur Promise qui accepte un rappel (au lieu d'une méthode sur l'API acceptant ce rappel) n'a rien à faire avec IoC. Promise introduit un niveau d'indirection utile pour la composition, le chaînage et la gestion des erreurs (en fait, la programmation orientée ferroviaire), mais le rappel n'est toujours pas exécuté par le client, il n'y a donc pas vraiment absence d'IoC.
dragan.stepanovic
1
@ dragan.stepanovic Vous avez raison, et j'ai utilisé la mauvaise terminologie. La différence est l'indirection. Avec un rappel, vous devez déjà savoir ce qui doit être fait avec le résultat. Avec une promesse, vous pouvez décider plus tard.
Cwharris