回调和Promises之间真的有根本的区别吗?


94

在执行单线程异步编程时,我熟悉两种主要技术。最常见的一种是使用回调。这意味着将传递给异步操作的函数的回调函数作为参数。当异步操作完成时,将调用回调。

jQuery这样设计一些典型的代码:

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

但是,当我们要在前一个完成后一个接一个地进行其他异步调用时,这种类型的代码可能会变得混乱且高度嵌套。

因此,第二种方法是使用Promises。Promise是一个对象,它表示可能不存在的值。您可以在其上设置回调,当准备读取值时将调用该回调。

Promises和传统的回调方法之间的区别在于,异步方法现在可以同步返回Promise对象,客户端将在其上设置回调。例如,在AngularJS中使用Promises的类似代码:

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

所以我的问题是:实际上有真正的区别吗?区别似乎纯粹是语法上的。

是否有更深层次的理由使用一种技术而不是另一种技术?


8
是的:回调只是一流的功能。Promises是monad,它提供一种可组合的机制来对值进行链式操作,并且碰巧将高阶函数与回调一起使用以提供方便的接口。
阿蒙2015年


5
@gnat:鉴于两个问题/答案的相对质量,重复投票应该是恕我直言的另一种方式。
Bart van Ingen Schenau 2015年

Answers:


110

可以说,承诺只是语法糖。您可以用诺言做的所有事情都可以用回调做。实际上,大多数promise实现都提供了在您需要时在两者之间进行转换的方法。

承诺通常更好的一个深层原因是它们更具可组合性,这大致意味着将多个promise组合起来“ 行之有效 ”,而将多个回调组合在一起通常不行。例如,将一个promise分配给一个变量并在以后附加附加的处理程序,或者甚至将一个处理程序附加到一大批promise上,只有在所有promise解析之后才执行,这很简单。虽然你可以排序的效仿这些东西回调,它需要更多的代码,是非常难以正确地做,而最终的结果通常是远远低于维护。

诺言获得可组合性的最大(也是最微妙)方法之一是对返回值和未捕获的异常的统一处理。对于回调,如何处理异常可能完全取决于将其嵌套的许多回调中的哪一个,以及采用回调的哪些函数在其实现中具有try / catch。使用promise,您知道将捕获一个逃避一个回调函数的异常并将其传递给您使用.error()或提供的错误处理程序.catch()

对于您给出的单个回调与单个promise的示例,确实没有显着差异。当您有不计其数的回调与不计其数的promise时,基于promise的代码往往看起来更好。


这是尝试用promise编写的一些假设代码,然后是回调,该回调应该足够复杂,以使您了解我在说什么。

有诺言:

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
Promise的另一个主要优点是,它们可以通过异步/等待或协程进一步“糖化”,该协程将yielded的Promise的值传递回去。这样做的好处是您可以混合本机控制流结构,这些结构可能会执行多少个异步操作。我将添加一个显示此内容的版本。
2016年

9
回调和promise之间的根本区别是控制的反转。使用回调,您的API必须接受回调,但是使用Promises,您的API必须提供promise。这是主要区别,对API设计具有广泛的意义。
cwharris '17

@ChristopherHarris不确定我是否同意。then(callback)在Promise上具有接受回调的方法(而不是在API上接受此回调的方法)无需对IoC进行任何操作。Promise引入了一种间接级别,该级别对于组合,链接和错误处理(实际上是面向铁路的编程)很有用,但是客户端仍然不执行回调,因此并不是真正没有IoC。
dragan.stepanovic

1
@ dragan.stepanovic你是对的,我使用了错误的术语。区别在于间接。使用回调,您必须已经知道需要对结果执行什么操作。有了承诺,您以后可以决定。
cwharris
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.