web-dev-qa-db-fra.com

Y a-t-il vraiment une différence fondamentale entre les rappels et les promesses?

Lors de la programmation asynchrone à un seul thread, il y a deux techniques principales que je connais. Le plus courant utilise les rappels. Cela signifie passer à la fonction qui agit de manière asynchrone une fonction de rappel comme paramètre. Une fois l'opération asynchrone terminée, le rappel sera appelé.

Un code jQuery typique conçu de cette façon:

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

Cependant, ce type de code peut devenir désordonné et très imbriqué lorsque nous voulons effectuer des appels asynchrones supplémentaires l'un après l'autre lorsque le précédent se termine.

Une deuxième approche consiste donc à utiliser Promises. 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 invoqués lorsque la valeur sera prête à être lue.

La différence entre Promises et l'approche traditionnelle des rappels est que les méthodes asynchrones renvoient désormais de manière synchrone les objets Promise, sur 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: y a-t-il réellement une réelle différence? La différence semble être purement syntaxique.

Y a-t-il une raison plus profonde d'utiliser une technique plutôt qu'une autre?

95
Aviv Cohn

Il est juste de dire que les promesses ne sont que du sucre syntaxique. Tout ce que vous pouvez faire avec des promesses que vous pouvez faire avec des rappels. En fait, la plupart des implémentations prometteuses offrent des moyens de conversion entre les deux quand vous le souhaitez.

La raison profonde pour laquelle les promesses sont souvent meilleures est qu'elles sont plus composables, ce qui signifie qu'en gros, la combinaison de plusieurs promesses "fonctionne", alors que la combinaison de plusieurs rappels ne fonctionne pas souvent. Par exemple, il est trivial 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 n'est exécuté qu'après la résolution de toutes les promesses. Bien que vous puissiez sorte d'émuler ces choses avec des rappels, cela prend beaucoup plus de code, est très difficile à faire correctement, et le résultat final est généralement loin moins maintenable.

L'un des moyens les plus importants (et les plus subtils) des promesses de gagner en composabilité est la gestion uniforme des valeurs de retour et des exceptions non capturées. Avec les rappels, la façon dont une exception est gérée peut dépendre entièrement de celui des nombreux rappels imbriqués qui l'a lancée et de la fonction qui prend des rappels a un try/catch dans son implémentation. Avec des promesses, vous savez qu'une exception qui échappe à une fonction de rappel sera interceptée et transmise au gestionnaire d'erreurs que vous avez fourni avec .error() ou .catch().

Pour l'exemple que vous avez donné d'un seul rappel par rapport à une seule promesse, il est vrai qu'il n'y a pas de différence significative. C'est lorsque vous avez un rappel de zillion par rapport à un zillion 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, puis avec des rappels qui devraient être juste assez 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);
                }
            });
        });
    });
}

Il pourrait y avoir des moyens intelligents de réduire la duplication de code dans la version des rappels même sans promesses, mais toutes celles auxquelles je peux penser se résument à implémenter quelque chose de très prometteur.

110
Ixrec