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

Posted

技术标签:

【中文标题】为啥回调比承诺更“紧密耦合”?【英文标题】:Why are callbacks more "tightly coupled" than promises?为什么回调比承诺更“紧密耦合”? 【发布时间】:2014-02-04 04:21:15 【问题描述】:

你能解释一下下面这句话(取自an answer to Stack Overflow question What are the differences between Deferred, Promise and Future in javascript?)吗?

使用jQuery promises 反对使用以前的 jQuery 回调有什么优点?

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

【问题讨论】:

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

promise 的耦合更加松散,因为操作不必“知道”它如何继续,它只需要知道它何时准备好。

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

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

因此,promise 有助于将链接事件与执行实际工作的关注点分开。

【讨论】:

"当你使用回调时,异步操作实际上有一个对其延续的引用,这不是它的事。" 仅当您以这种方式编写代码时。在像 C++ 这样的老式语言中执行此操作的“正确”方法是让 showLoadingScreen 代码等待加载完成时收到通知的条件变量。也许更多的代码,但如果你选择正确的习语,与传统语言的耦合肯定是低的。 @BillAtHRST,“等待加载完成时收到通知的条件变量”——我不确定我是否理解。如果不是基本上通过回调/承诺,你将如何在 javscript 中做到这一点?【参考方案2】:

A promise is an object that represents the result of an asynchronous operation,因此您可以传递它,这为您提供了更大的灵活性。

如果您使用回调,则在调用异步操作时,您必须指定如何处理它,从而实现耦合。使用 Promise,您可以指定以后如何处理它。

这里有一个例子,假设你想通过 ajax 加载一些数据,同时你想显示一个加载页面。

带有回调:

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

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

使用 promises,您可以重写上面的 sn-p,使其更具可读性,并且您不必将 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
;

更新:我写了一个blog post,它提供了额外的示例,并清楚地说明了什么是承诺,以及如何将它的使用与使用回调进行比较。

【讨论】:

在回调示例中hideLoadingScreendoSomethingWithTheData 之后执行,但在promise 示例中hideLoadingScreendoSomethingWithTheData 之前执行。我假设如果返回数据以注入到 DOM 中,那么加载屏幕的隐藏应该在之后发生,就像回调一样。可能应该更新 promise 示例以反映与回调示例相同的结果。 这些例子并不完全等价,通过提取回调版本的 getData 你可以获得同样的好处,gist.github.com/opsb/9413093 @Rui by composability 你指的是promise的可链性吗?你可以用同样的方式编写回调函数,唯一的区别是你最终会得到可怕的回调金字塔(每个调用都缩进另一个调用)。 @Rui Martin 指的是稍微不同的东西,这实际上是 Promise 的一个巨大优势,你可以并行化它们,他的例子是:composedPromise = $.when(anAsyncFunction(), anotherAsyncFunction() ); @opsb:与嵌套回调相比,链接 Promise 有一个(巨大的)优势:您可以链接在其他地方定义的函数。这意味着您可以在多个位置使用相同的功能。【参考方案3】:

他们不是,这只是一种合理化,完全忽略了承诺的人用它来证明编写比使用回调编写更多的代码是合理的。鉴于这样做显然没有任何好处,您至少可以始终告诉自己代码耦合度较低或其他什么。

请参阅what are promises and why should I use them 了解实际的具体好处。

【讨论】:

我认为耦合参数对浏览器有效,在浏览器中没有像节点cb(err,result) 这样的回调参数的约定。如果您的所有代码都使用相同的回调约定(不同于一般的 Backbone 或浏览器 API),那么是的,它的耦合度不亚于 Promise。 @timruffles 与其他承诺的论点相比,它是如此微不足道......这就是 Petka 在这里所说的。 -1:除了不正确之外,答案是火焰抑制。正如 harpo 上面指出的那样,它可以让您将回调与回调的延续分离。 @JørgenFogh 回调也很简单,这就是为什么它不是一个好的承诺点 嘲笑别人是不恰当的,即使他们实际上是错误的。这就是为什么我称之为火焰诱饵。【参考方案4】:

我不认为 promises 与回调相比或多或少是耦合的,几乎相同。

不过,Promise 还有其他好处:

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

无法在其上调用回调抛出和异常,因此您必须为错误情况提供另一个回调。

只能注册一个回调,一个以上的 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++ 中的类似代码耦合更糟糕,因为那里的字符串实现不同,使其更具可重用性。有了标准的承诺,调用者和实现之间的耦合度就会降低,因为它们不必就具有自定义参数顺序、名称等的(一对)自定义回调达成一致......事实上有很多不同的承诺的味道无助于思考。

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

  var cache:  [key: string] : Promise<any> ;

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

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

【讨论】:

这些好处要么是微不足道的,要么不是特定于 Promise 的。关键是要有一个内部 DSL 来编写异步代码,这与编写同步代码的方式基本相同。 当然可以,但是如果没有yield / async 语法,解决方案是不支持的。我发现他们编写 Typescript 很有用,其中有类型声明,而不是 javascript。 并非如此,使用yield,您会被语法try-catch 卡住,而使用箭头函数时,代码行数和详细程度保持不变。 这里有一个例子,我的意思是被语法 try-catch 卡住,以及与 promises 相比,yield 并不是那么好:pastebin.com/h7aD54vQ var current = $.when(null); images.forEach(i => current = current.then(_ => $.ajax("image/" + i)));【参考方案5】:

承诺将延迟响应的概念具体化。 它们使异步计算成为一等公民,因为您可以传递它。它们允许您根据需要定义结构 - 即一元结构 - 您可以在其上构建大大简化代码的高阶组合器。

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

现在换一种方式来回答您的问题。回调是一种临时解决方案,其中 Promise 允许更清晰的结构和可重用性。

【讨论】:

以上是关于为啥回调比承诺更“紧密耦合”?的主要内容,如果未能解决你的问题,请参考以下文章

如何将我的代码从回调函数更改为承诺 [重复]

Promise承诺对象

为啥没有触发`.catch`回调

承诺回调返回承诺

如何在回调结束时获得通知承诺所有回调样式[重复]

了解回调和承诺。它是不是正确?