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

一些典型的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);
    });


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

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

评论

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

JS Async的可能重复项:。我是否可以完全忘记回调,并用Promise和/或Generators代替

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

#1 楼

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

promise通常更好的深层原因是它们更具可组合性,这大致意味着将多个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);
                }
            });
        });
    });
}


即使没有承诺,也可能有一些聪明的方法可以减少回调版本中的代码重复,但是我能想到的所有方法都归结为实现非常像承诺的东西。

评论


Promise的另一个主要优点是它们可以通过异步/等待或协程进一步“糖化”,该协程可以将已承诺的值传回给已产生的Promise。这样做的好处是您可以混合本机控制流结构,这些结构可能会执行多少次异步操作。我将添加一个显示此内容的版本。

–acjay
16-10-25在18:38

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

– cwharris
17年2月7日在22:24



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

–dragan.stepanovic
18-2-17在21:38



@ dragan.stepanovic你是对的,我使用了错误的术语。区别在于间接。使用回调,您必须已经知道需要对结果执行什么操作。有了承诺,您以后可以决定。

– cwharris
18-2-17在23:14