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

37

2
“……像什么普通的Javascript代码?JavaScript Promise是ES6的一部分。这里可以查看更多详细信息:http://www.html5rocks.com/en/tutorials/es6/promises/ 从一般意义上讲,与回调函数相比,抽象化和解耦程度越高,您就可以更好地测试代码并分离关注点(请注意,过度抽象化可能会导致其他问题)。Promises允许您拥有一个单一的重点操作,而无需知道之前发生了什么或之后会发生什么。” - Nope
1
当您注册一个 Promise 时,参数顺序的依赖性会降低。考虑一下,当您想要将函数声明从 func(arg1, arg2, callback) 更改为 func(arg1, arg2, [optional]arg3, callback) 时。 - megawac
5个回答

51

承诺(Promise)是代表异步操作结果的对象,因此你可以传递它,并且这使你更加灵活。

如果使用回调函数,在调用异步操作时必须指定如何处理它,因此有了耦合。使用 Promise 可以稍后指定如何处理。

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

使用回调函数:

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

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

如果使用Promise,可以重写上面的片段,使其更易读,并且不必将hideLoadingScreen放在完成回调中。

使用Promise

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
};

更新:我已经写了一篇博客文章,提供了额外的示例,并清晰地描述了Promise是什么以及如何将其使用与使用回调进行比较。


17
这些示例并不完全等价,通过将 getData 提取到回调函数版本中,您可以获得同样的好处。 https://gist.github.com/opsb/9413093 - opsb
@opsb 但是你无法像getData函数在显示/隐藏加载屏幕方面利用Promise的可组合性,而回调函数则不行。 - Rui
1
@Rui,你提到的可组合性是指 Promise 的链式调用吗?你可以以同样的方式组合回调函数,唯一的区别是你最终会得到令人头疼的回调金字塔(每个调用都缩进在另一个调用中)。 - opsb
1
@Rui Martin提到了一些略微不同的东西,实际上这是Promise的巨大优势之一,您可以将它们并行化,他的例子是:composedPromise = $.when(anAsyncFunction(), anotherAsyncFunction()); - opsb
1
@opsb:链式调用 Promise 相对于嵌套回调有一个(巨大的)优势:你可以链接在其他地方定义的函数。这意味着你可以在多个位置使用相同的函数。 - Jørgen Fogh
显示剩余3条评论

27

使用 Promises 时,由于操作不需要“知道”它如何继续,只需知道何时准备好,因此与回调函数相比,耦合度更低。

当您使用回调函数时,异步操作实际上具有对其继续执行的引用,而这不是它的职责。

使用 Promises,您甚至可以在决定如何解决问题之前,轻松地对异步操作创建表达式。

因此,Promises 帮助分离链接事件和执行实际工作的关注点。


当您使用回调函数时,异步操作实际上具有对其继续运行的引用,而这并不是其业务。只有在您以这种方式编写代码时才会出现这种情况。在像C++这样的老式语言中,"适当"的做法是让showLoadingScreen代码等待一个条件变量,在加载完成时得到通知。也许需要更多的代码,但如果选择正确的习惯用语,与传统语言的低耦合性肯定是可能的。 - BillT
@BillAtHRST,“等待在一个条件变量上,当加载完成时得到通知”——我不确定我理解了。如果不是通过回调/承诺实现,你会如何在JavaScript中做到这一点? - harpo

12

我认为承诺和回调函数的耦合程度几乎相同,没有更多或更少。

但是,承诺有其他好处:

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

  • 无法在回调函数中抛出异常并使其停止执行,因此必须为错误情况提供另一个回调函数。

  • 只能注册一个回调函数,但可以注册多个承诺,而且可以在事件之后注册它们,而且仍然会被调用。

  • 在类型声明(TypeScript)中,使用承诺更容易阅读签名。

  • 未来可以利用异步/ 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++中类似的代码更加松耦合,因为在C++中由于字符串的不同实现,使得它更具可重用性。拥有标准的promise后,调用者和实现彼此之间的耦合度较小,因为它们不必对自定义回调函数及其参数顺序、名称等达成一致。然而,存在许多不同版本的promise并不太有帮助。

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

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

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

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


1
这些好处要么微不足道,要么与 Promises 无关。重点是拥有一种内部 DSL,以便编写异步代码的方式与编写同步代码的方式大致相同。 - Esailija
2
实际上,使用 yield 你会被限制在语法 try-catch 中,而使用箭头函数时,代码行数和冗长程度保持不变。 - Esailija
1
这是一个关于被语法上的try-catch所困扰以及与Promise相比yield并不是那么好的例子:http://pastebin.com/h7aD54vQ - Esailija
1
变量 current = $.when(null);,images.forEach(i => current = current.then(_ => $.ajax("image/" + i))); - Gjorgi Kjosev
我已经创建了这个函数: export function promiseForeach<T>(array: T[], action: (elem: T) => Promise<void>): Promise<void> { return array.reduce<Promise<void>>( (prom, val) => prom.then(() => action(val)), Promise.resolve<void>(null)); } 但我的观点是,它们是非平凡的转换。添加Try/Catch和一些条件,我们可能需要再等待20天,因此使用async/await是有意义的。 - Olmo
显示剩余6条评论

5
承诺(Promises)实现了对某些东西延迟响应的概念。由于您可以传递它们,因此它们使异步计算成为一级公民。如果您想要定义结构,它们允许您定义单子结构,并在其上构建高阶组合子,从而极大地简化了代码。
例如,您可以编写一个函数,该函数接受一组承诺并返回一个承诺数组(通常称为“序列”)。使用回调非常难以做到这点,甚至是不可能的。这样的组合子不仅使代码更易于编写,而且使代码更易于阅读。
现在考虑反过来回答您的问题。回调是一种临时解决方案,承诺则允许更清晰的结构和可重用性。

4
他们并不是这样的,这只是那些完全没有理解承诺的人为了编写比使用回调函数更多的代码而进行的一种合理化的行为。考虑到这样做显然没有任何好处,你至少可以告诉自己代码更加松耦合之类的理由。
请参见什么是Promise以及为什么要使用它们以获取实际的具体好处。

我认为耦合参数对于浏览器是有效的,因为没有回调参数的约定,例如节点 cb(err,result)。如果您的所有代码都使用相同的回调约定(不像 Backbone 或一般的浏览器 API),那么是的,它与 promises 一样耦合。 - timruffs
@timruffles 这只是与 Promise 的其他参数相比微不足道...这就是 Petka 在这里所说的。 - Benjamin Gruenbaum
-1:答案是煽动性的,而且是错误的。正如Harpo在上面指出的那样,它让你将回调与回调的继续解耦。 - Jørgen Fogh
@JørgenFogh,使用回调函数也非常简单,这就是为什么它不是 Promise 的优点。 - Esailija
嘲笑别人犯错误是不合适的,即使他们真的犯错了。这就是为什么我把它称为“引战贴”。 - Jørgen Fogh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接