为什么回调比承诺更“紧密耦合”?

Why are callbacks more "tightly coupled" than promises?

提问人:Revious 提问时间:1/15/2014 最后编辑:CommunityRevious 更新时间:1/19/2016 访问量:15876

问:

你能解释一下下面这句话吗(摘自 Stack Overflow 问题的答案 Javascript 中的 Deferred、Promise 和 Future 有什么区别?

与使用以前的 jQuery 回调相比,使用 jQuery promise 有什么好处?

而不是直接将回调传递给函数,而是 可以导致紧密耦合的接口,使用 Promise 可以允许 对同步或异步代码的单独关注点。

JavaScript jQuery 回调 承诺

评论

2赞 Nope 1/15/2014
...normal Javascript code比如?JavaScript promise 只是 ES6 原生的一部分。有关更多详细信息,请参阅此处: html5rocks.com/en/tutorials/es6/promises 关于一般意义上的回调与承诺,随着抽象和解耦的越多,您就越能更好地测试您的代码并分离关注点(请注意,您可能会过度抽象,但那是另一回事)。Promise 允许您进行单个焦点操作,而不必知道之前或之后会发生什么。
1赞 megawac 1/15/2014
注册 promise 时,对参数顺序的依赖性较小。考虑何时要将函数的声明从 更改为func(arg1, arg2, callback)func(arg1, arg2, [optional]arg3, callback)

答:

27赞 harpo 1/15/2014 #1

耦合与承诺更松散,因为操作不必“知道”它如何继续,它只需要知道何时准备就绪。

当你使用回调时,异步操作实际上有一个对它的延续的引用,这不是它的业务。

借助 Promise,您甚至可以在决定如何解析表达式之前,轻松地通过异步操作创建表达式。

因此,承诺有助于将链接事件与执行实际工作的问题分开。

评论

0赞 BillT 3/8/2014
“当你使用回调时,异步操作实际上有一个对它的延续的引用,这不是它的业务。” <br/> 只有当你以这种方式编写代码时。在像 C++ 这样的老式语言中执行此操作的“正确”方法是让 showLoadingScreen 代码等待在加载完成时收到通知的条件变量。也许更多的代码,但如果你选择正确的习惯用语,肯定有可能与传统语言有低耦合。
0赞 harpo 3/8/2014
@BillAtHRST,“等待加载完成后收到通知的条件变量”——我不确定我是否理解。如果不是基本上通过回调/承诺,您将如何在 javscript 中做到这一点?
51赞 Rui 1/16/2014 #2

promise 是一个对象,它表示异步操作的结果,因此您可以传递它,这为您提供了更大的灵活性。

如果使用回调,则在调用异步操作时,必须指定如何处理它,因此需要耦合。使用 promise,您可以指定以后如何处理它。

下面是一个示例,假设您想通过 ajax 加载一些数据,同时您想显示一个加载页面。

使用回调:

void loadData = function(){
  showLoadingScreen();
  $.ajax("http://someurl.com", {
    complete: function(data){
      hideLoadingScreen();
      //do something with the data
    }
  });
};

处理返回数据的回调必须调用 hideLoadingScreen。

使用 promises,您可以重写上面的代码片段,使其更具可读性,并且您不必将 hideLoadingScreen 放在完整的回调中。

有承诺

var getData = function(){
  showLoadingScreen();
  return $.ajax("http://someurl.com").promise().always(hideLoadingScreen);
};

var loadData = function(){
  var gettingData = getData();
  gettingData.done(doSomethingWithTheData);
}

var doSomethingWithTheData = function(data){
 //do something with data
};

更新:我写了一篇博文,提供了额外的例子,并清楚地描述了什么是承诺,以及如何将其使用与使用回调进行比较。

评论

0赞 Nope 1/16/2014
在回调示例中,执行时间晚于 ,但在 promise 示例中执行时间在 之前。我假设如果返回数据以注入 DOM,则加载屏幕的隐藏应该在之后发生,就像回调一样。Promise 示例可能应该更新,以反映与回调示例相同的结果。hideLoadingScreendoSomethingWithTheDatahideLoadingScreendoSomethingWithTheData
17赞 opsb 3/7/2014
这些示例并不真正等价,通过提取回调版本的 getData,您可以获得相同的好处,gist.github.com/opsb/9413093
1赞 opsb 3/7/2014
@Rui,你所说的可组合性是指承诺的可链性吗?你可以用同样的方式编写回调函数,唯一的区别是你最终会得到可怕的回调金字塔(每个调用都在另一个调用中缩进)。
1赞 opsb 3/7/2014
@Rui Martin 指的是一些稍微不同的东西,这实际上是 promise 的一个巨大优势,你可以并行化它们,他的例子是: composedPromise = $.when(anAsyncFunction(), anotherAsyncFunction());
1赞 Jørgen Fogh 5/20/2014
@opsb:与嵌套回调相比,链接 promise 有一个(巨大的)优势:您可以链接在其他地方定义的函数。这意味着您可以在多个位置使用相同的功能。
4赞 Esailija 3/7/2014 #3

他们不是,这只是一种合理化,那些完全错过了承诺点的人用它来证明编写比使用回调编写的代码多得多的代码是合理的。鉴于这样做显然没有好处,你至少可以总是告诉自己代码耦合较少或其他什么。

了解什么是承诺,以及我为什么要将它们用于实际的具体利益。

评论

0赞 timruffs 3/8/2014
我认为耦合参数对浏览器有效,其中没有像节点这样的回调参数的约定。如果你的所有代码都使用相同的回调约定(不像一般的Backbone或浏览器API),那么是的,它的耦合程度不亚于承诺。cb(err,result)
0赞 Benjamin Gruenbaum 3/21/2014
@timruffles与其他承诺论据相比,它是如此微不足道......这就是佩特卡在这里所说的。
0赞 Jørgen Fogh 5/20/2014
-1:答案是火焰蝙蝠,除了不正确之外。正如 harpo 在上面指出的那样,它允许您将回调与回调的延续分离。
0赞 Esailija 5/20/2014
@J ørgenFogh 回调也非常微不足道,这就是为什么它不是一个好的承诺点
0赞 Jørgen Fogh 5/20/2014
嘲笑别人弄错了是不恰当的,即使他们实际上弄错了。这就是为什么我称它为火焰诱饵。
12赞 Olmo 3/7/2014 #4

我不认为承诺或多或少与回调耦合,只是差不多。

然而,承诺还有其他好处:

  • 如果公开一个回调,你必须记录它是被调用一次(如在jQuery.ajax中)还是多次(如在Array.map中)。承诺总是被调用一次。

  • 没有办法调用回调抛出和异常,因此您必须为错误情况提供另一个回调。

  • 只需注册一个回调,多个 promise 回调,您可以在活动结束后注册它们,无论如何您都会被调用。

  • 在类型化声明 (Typescript) 中,Promise 使签名更易于阅读。

  • 将来,您可以利用 async / yield 语法。

  • 因为它们是标准的,所以您可以制作可重用的组件,如下所示:

     disableScreen<T>(promiseGenerator: () => Promise<T>) : Promise<T>
     {
         //create transparent div
         return promiseGenerator.then(val=>
         {
            //remove transparent div
            return val;
         }, error=>{
             //remove transparent div
             throw error;
         });
     }
    
     disableScreen(()=>$.ajax(....));
    

更多信息:http://www.html5rocks.com/en/tutorials/es6/promises/

编辑:

  • 另一个好处是编写 N 个异步调用序列,而没有 N 级缩进。

另外,虽然我仍然不认为这是重点,但现在我认为由于以下原因,它们耦合得更松散一些:

  • 它们是标准的(或者至少是尝试的):使用字符串的 C# 或 Java 代码比 C++ 中的类似代码更糟糕,因为那里的字符串实现不同,使其更可重用。有了标准承诺,调用方和实现之间的耦合程度较低,因为它们不必就具有自定义参数 orders、names 等的(一对)自定义回调达成一致......事实上,承诺有许多不同的口味,这无助于思考。

  • 它们促进了更基于表达式的编程,更易于组合、缓存等:

      var cache: { [key: string] : Promise<any> };
    
      function getData(key: string): Promise<any> {
          return cache[key] || (cache[key] = getFromServer(key)); 
      }
    

你可以说,基于表达式的编程比基于命令式/回调的编程更松散地耦合,或者至少它们追求相同的目标:可组合性。

评论

1赞 Esailija 3/8/2014
这些好处要么是微不足道的,要么不是特定于承诺的。关键是要有一个内部 DSL 来编写异步代码,与编写同步代码的方式大致相同。
0赞 Olmo 3/8/2014
当然,但确实,如果没有 yield / async 语法,解决方案是半支持的。我发现它们在编写 Typescript 时很有用,其中有类型声明,而不是在 javascript 上。
2赞 Esailija 3/8/2014
并非如此,您被语法 try-catch 卡住了,而使用箭头函数,代码行和冗长程度保持相同水平。yield
1赞 Esailija 3/8/2014
这里有一个例子,说明我所说的被句法 try-catch 所困住的意思,以及与承诺相比,yield 并不是那么好: pastebin.com/h7aD54vQ
1赞 Gjorgi Kjosev 3/26/2014
var 电流 = $.when(null);images.forEach(i => current = current.then(_ => $.ajax(“image/” + i)));
5赞 edofic 3/8/2014 #5

承诺重申了对某事的延迟响应的概念。 它们使异步计算成为一等公民,因为您可以传递它。它们允许您根据需要定义结构 - 一元结构 - 您可以在此基础上构建高阶组合器,从而大大简化代码。

例如,你可以有一个函数,它接受一个 promise 数组并返回一个数组的 promise(通常称为 )。这很难做到,甚至不可能通过回调。这样的组合器不仅使代码更易于编写,而且使代码更易于阅读。sequence

现在反过来考虑一下来回答你的问题。回调是一种临时解决方案,其中承诺允许更清晰的结构和可重用性。