承诺不就是回调吗?

Aren't promises just callbacks?

提问人:Benjamin Gruenbaum 提问时间:3/21/2014 最后编辑:Benjamin Gruenbaum 更新时间:3/27/2023 访问量:127471

问:

我开发 JavaScript 已经有几年了,我完全不明白 promise 有什么大惊小怪的。

似乎我所做的只是改变:

api(function(result){
    api2(function(result2){
        api3(function(result3){
             // do work
        });
    });
});

无论如何,我都可以使用像 async 这样的库,如下所示:

api().then(function(result){
     api2().then(function(result2){
          api3().then(function(result3){
               // do work
          });
     });
});

这更多的代码和更低的可读性。我在这里什么也没得到,它也不是突然神奇地“平坦”了。更不用说必须将事情转化为承诺了。

那么,这里的承诺有什么大惊小怪的呢?

javascript callback promise q bluebird

评论

12赞 ComFreek 3/21/2014
主题:Html5Rocks 上有一篇关于 Promise 的非常翔实的文章:html5rocks.com/en/tutorials/es6/promises
2赞 Esailija 3/31/2014
仅供参考,您接受的答案是相同的琐碎好处的旧列表,这些好处根本不是承诺的重点,甚至没有说服我使用承诺:/。说服我使用承诺的是 Oscar 回答中描述的 DSL 方面
0赞 Benjamin Gruenbaum 4/9/2014
@Esailija没事,你的韭菜说话说服了我。我已经接受了另一个答案,尽管我认为 Bergi 的答案也提出了一些非常好(和不同)的观点。
1赞 monsto 3/3/2018
@Esailija “说服我使用承诺的是 Oscar 回答中描述的 DSL 方面” << 什么是“DSL”?你所指的“DSL方面”是什么?
3赞 Michael Ekoka 4/11/2018
@monsto:DSL:领域特定语言,一种专门设计用于系统特定子集的语言(例如.SQL或ORM与数据库通信,正则表达式查找模式等)。在这种情况下,“DSL”是 Promise 的 API,如果你像 Oscar 那样构建代码,它几乎就像语法糖一样,它补充了 JavaScript 以解决异步操作的特定上下文。Promise 创造了一些习语,将它们几乎变成了一种语言,旨在让程序员更容易掌握这种结构中有些难以捉摸的思维流程。

答:

710赞 Oscar Paz 3/21/2014 #1

承诺不是回调。promise 表示异步操作的未来结果。当然,按照你的方式写它们,你得到的好处很少。但是,如果按照预期使用的方式编写它们,则可以以类似于同步代码的方式编写异步代码,并且更易于遵循:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
});

当然,代码不会少得多,但可读性要高得多。

但这还不是结束。让我们来了解一下真正的好处:如果您想检查任何步骤中的任何错误怎么办?用回调来做这件事会很糟糕,但用承诺来做,是小菜一碟:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
});

与块几乎相同。try { ... } catch

更好的是:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
}).then(function() {
     //do something whether there was an error or not
     //like hiding an spinner if you were performing an AJAX request.
});

更好的是:如果这 3 个对 的调用可以同时运行(例如,如果它们是 AJAX 调用),但您需要等待这三个调用怎么办?如果没有承诺,您应该必须创建某种计数器。有了 promises,使用 ES6 符号,是另一回事,而且非常简洁:apiapi2api3

Promise.all([api(), api2(), api3()]).then(function(result) {
    //do work. result is an array contains the values of the three fulfilled promises.
}).catch(function(error) {
    //handle the error. At least one of the promises rejected.
});

希望你现在以新的眼光看待应许。

评论

146赞 Pacerier 5/3/2014
他们真的不应该将其命名为“承诺”。“未来”至少要好 100 倍。
14赞 Esailija 1/31/2015
@Pacerier因为 Future 没有被 jQuery 污染?
12赞 Dtipson 12/23/2015
替代模式(取决于所需的模式:api().then(api2).then(api3).then(doWork);也就是说,如果 api2/api3 函数从最后一步获取输入,并自行返回新的 promise,则无需额外包装即可链接它们。也就是说,他们组成。
1赞 NiCk Newman 1/25/2016
如果 和 中有异步操作怎么办?是否只有在这些异步操作完成后才会调用最后一个?api2api3.then
2赞 Déjà vu 3/27/2020
我发现这个答案是关于如何使用承诺的最好例子之一。所以,我不明白沉重的反对票?(截至今天-6)
198赞 Bergi 3/21/2014 #2

是的,Promise 是异步回调。它们不能做回调做不到的事情,而且异步和普通回调一样,你也会面临同样的问题。

但是,Promise 不仅仅是回调。它们是一个非常强大的抽象,允许更干净、更好的功能性代码,并且不容易出错。

那么主要思想是什么?

Promise 是表示单个(异步)计算结果的对象。他们只解决一次该结果。这意味着以下几点:

Promise 实现了一个观察者模式:

  • 在任务完成之前,您无需知道将使用该值的回调。
  • 与其期望回调作为函数的参数,不如轻松地将 Promise 对象return
  • promise 将存储值,您可以随时透明地添加回调。当结果可用时,将调用它。“透明性”意味着当你有一个承诺并为其添加回调时,结果是否已经到达对你的代码没有影响 - API 和合约是相同的,大大简化了缓存/记忆。
  • 您可以轻松添加多个回调

Promise 是可链接的(monadic如果你愿意的话):

  • 如果需要转换 promise 所表示的值,请在 promise 上映射一个转换函数,并返回表示转换结果的新 promise。您无法同步获取值以某种方式使用它,但您可以轻松地在 promise 上下文中提升转换。没有样板回调。
  • 如果要链接两个异步任务,可以使用该方法。它将需要一个回调来调用第一个结果,并返回回调返回的 promise 结果的 promise。.then()

听起来很复杂?是时候进行代码示例了。

var p1 = api1(); // returning a promise
var p3 = p1.then(function(api1Result) {
    var p2 = api2(); // returning a promise
    return p2; // The result of p2 …
}); // … becomes the result of p3

// So it does not make a difference whether you write
api1().then(function(api1Result) {
    return api2().then(console.log)
})
// or the flattened version
api1().then(function(api1Result) {
    return api2();
}).then(console.log)

扁平化并不神奇,但你可以很容易地做到。对于高度嵌套的示例,(接近)等效项为

api1().then(api2).then(api3).then(/* do-work-callback */);

如果查看这些方法的代码有助于理解,这里有一个最基本的 promise 库,只有几行

承诺有什么大惊小怪的?

Promise 抽象允许更好的函数可组合性。例如,在 for chaining 旁边,该函数为多个并行等待 promise 的组合结果创建一个 promise。thenall

最后但并非最不重要的一点是,Promise 带有集成的错误处理。计算的结果可能是,要么用一个值实现承诺,要么用一个理由拒绝它。所有组合函数都会自动处理这个问题,并在 promise 链中传播错误,因此您不需要在任何地方显式地关心它 - 与普通回调实现相反。最后,您可以为所有发生的异常添加专用的错误回调。

更不用说必须将事情转化为承诺了。

对于好的 promise 库来说,这实际上是微不足道的,请参阅如何将现有的回调 API 转换为 promise?

评论

1赞 Sebastien Lorber 4/1/2014
嗨,Bergi,你对这个 SO 问题有什么有趣的东西要补充吗?stackoverflow.com/questions/22724883/......
1赞 Bergi 4/1/2014
@Sebastien:我对 Scala 了解不多,我只能重复 Benjamin 所说的话:-)
4赞 Tamas Hegedus 11/20/2015
只是一点评论:您不能使用 ,因为 console.log 取决于控制台上下文。这样一来,就会导致非法调用错误。使用 or 绑定上下文。.then(console.log)console.log.bind(console)x => console.log(x)
3赞 Bergi 11/20/2015
@hege_hegedus:有些环境已经绑定了方法。当然,我只是说两个嵌套具有完全相同的行为,并不是说它们中的任何一个都可以工作:-Pconsole
1赞 Adam Patterson 3/6/2017
那太好了。这就是我所需要的:更少的代码和更多的解释。谢谢。
5赞 Kjell Schubert 4/1/2014 #3

Promise 不是回调,两者都是促进异步编程的编程习惯用语。使用返回 promise 的协程或生成器使用异步/等待风格的编程可以被认为是第三个这样的习语。这些习语在不同编程语言(包括 Javascript)中的比较如下: https://github.com/KjellSchubert/promise-future-task

3赞 Apoorv 6/20/2016 #4

没有承诺只是回调的包装器

例 您可以将 javascript 原生承诺与 node js 一起使用

my cloud 9 code link : https://ide.c9.io/adx2803/native-promises-in-node

/**
* Created by dixit-lab on 20/6/16.
*/

var express = require('express');
var request = require('request');   //Simplified HTTP request client.


var app = express();

function promisify(url) {
    return new Promise(function (resolve, reject) {
    request.get(url, function (error, response, body) {
    if (!error && response.statusCode == 200) {
        resolve(body);
    }
    else {
        reject(error);
    }
    })
    });
}

//get all the albums of a user who have posted post 100
app.get('/listAlbums', function (req, res) {
//get the post with post id 100
promisify('http://jsonplaceholder.typicode.com/posts/100').then(function (result) {
var obj = JSON.parse(result);
return promisify('http://jsonplaceholder.typicode.com/users/' + obj.userId + '/albums')
})
.catch(function (e) {
    console.log(e);
})
.then(function (result) {
    res.end(result);
}
)

})


var server = app.listen(8081, function () {

var host = server.address().address
var port = server.address().port

console.log("Example app listening at http://%s:%s", host, port)

})


//run webservice on browser : http://localhost:8081/listAlbums
27赞 John Weisz 9/13/2016 #5

除了已经确定的答案之外,通过ES6箭头函数,Promise从一颗不起眼的小蓝矮星直接变成了一颗红巨星。那即将坍缩成一颗超新星:

api().then(result => api2()).then(result2 => api3()).then(result3 => console.log(result3))

正如 oligofren 所指出的,如果没有 api 调用之间的参数,你根本不需要匿名包装函数:

api().then(api2).then(api3).then(r3 => console.log(r3))

最后,如果你想达到超大质量黑洞的水平,可以等待承诺:

async function callApis() {
    let api1Result = await api();
    let api2Result = await api2(api1Result);
    let api3Result = await api3(api2Result);

    return api3Result;
}

评论

8赞 Michael McGinnis 10/31/2017
这使得应许听起来像是一场宇宙灾难,我认为这不是你的本意。
1赞 oligofren 11/8/2017
如果不使用方法中的参数,则不妨完全跳过箭头函数:.apiXapi().then(api2).then(api3).then(r3 => console.log(r3))
0赞 John Weisz 5/24/2018
@MichaelMcGinnis -- Promises对沉闷的回调地狱的有益影响就像太空黑暗角落里爆炸的超新星。
0赞 Dmitri Zaitsev 6/9/2019
我知道你的意思是诗意的,但承诺与“超新星”相去甚远。我脑海中浮现出违反一元定律或缺乏对更强大用例(如取消或返回多个值)的支持。
15赞 Duncan Lukkenaer 7/23/2017 #6

除了其他答案之外,ES2015 语法还与 promise 无缝融合,从而减少了更多的样板代码:

// Sequentially:
api1()
  .then(r1 => api2(r1))
  .then(r2 => api3(r2))
  .then(r3 => {
      // Done
  });

// Parallel:
Promise.all([
    api1(),
    api2(),
    api3()
]).then(([r1, r2, r3]) => {
    // Done
});
24赞 Karthick 4/18/2018 #7

除了上面令人敬畏的答案外,还可以添加 2 点:

1.语义差异:

承诺可能在创建时就已经解决了。这意味着它们保证的是条件而不是事件。如果它们已被解析,则仍会调用传递给它的已解析函数。

相反,回调处理事件。因此,如果您感兴趣的事件发生在注册回调之前,则不会调用回调。

2. 控制反转

回调涉及控制反转。当您向任何 API 注册回调函数时,Javascript 运行时会存储回调函数,并在准备好运行后从事件循环中调用它。

有关说明,请参阅 Javascript 事件循环

使用 Promises,控制权属于调用程序。如果我们存储 promise 对象,则可以随时调用 .then() 方法

评论

1赞 radiantshaw 2/26/2019
我不知道为什么,但这似乎是一个更好的答案。
0赞 HopeKing 4/10/2021
很好,这个 ->“有了 Promises,控制权就掌握在调用程序中。如果我们存储 promise 对象,则可以随时调用 .then() 方法。
0赞 Ramy Farid 9/20/2022
这应该是正确答案,尤其是语义差异部分。这在我脑海中已经有一段时间了,但无法用语言表达,但这就是 promise 和回调之间的核心区别
1赞 Hamid Shoja 12/20/2019 #8

JavaScript Promise 实际上使用回调函数来确定在 Promise 被解析或拒绝后要做什么,因此两者没有根本区别。Promise 背后的主要思想是接受回调 - 尤其是嵌套回调,你想执行某种操作,但它会更具可读性。

2赞 Willem van der Veen 5/6/2020 #9

承诺概述:

在 JS 中,我们可以将异步操作(例如数据库调用、AJAX 调用)包装在 promise 中。通常,我们希望对检索到的数据运行一些额外的逻辑。JS promise 具有处理异步操作结果的处理程序函数。处理程序函数甚至可以在其中具有其他异步操作,这些操作可能依赖于先前异步操作的值。

承诺始终具有以下 3 种状态:

  1. 待定:每个承诺的起始状态,既未兑现也不被拒绝。
  2. fulfilled:操作已成功完成。
  3. rejected:操作失败。

可以使用值解析/完成或拒绝挂起的 promise。然后调用以下将回调作为参数的处理程序方法:

  1. Promise.prototype.then():当 promise 解析时,将调用此函数的回调参数。
  2. Promise.prototype.catch():当 promise 被拒绝时,将调用此函数的回调参数。

虽然上述方法技能获取回调参数,但远胜于使用 这里只有回调是一个可以澄清很多的例子:

function createProm(resolveVal, rejectVal) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                console.log("Resolved");
                resolve(resolveVal);
            } else {
                console.log("Rejected");
                reject(rejectVal);
            }
        }, 1000);
    });
}

createProm(1, 2)
    .then((resVal) => {
        console.log(resVal);
        return resVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
        return resVal + 2;
    })
    .catch((rejectVal) => {
        console.log(rejectVal);
        return rejectVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
    })
    .finally(() => {
        console.log("Promise done");
    });

  • createProm 函数创建一个 promise,该 promise 在 1 秒后根据 random Nr 解析或拒绝
  • 如果 promise 已解析,则调用第一个方法,并将解析的值作为回调的参数传入then
  • 如果 promise 被拒绝,则调用第一个方法,并将被拒绝的值作为参数传入catch
  • 和方法返回 promise,这就是我们可以链接它们的原因。它们将任何返回值包装在 中,并将任何抛出的值(使用关键字)包装在 中。因此,返回的任何值都会转换为 promise,并且在此 promise 上,我们可以再次调用处理函数。catchthenPromise.resolvethrowPromise.reject
  • 与嵌套回调相比,Promise 链为我们提供了更精细的控制和更好的概览。例如,该方法处理在处理程序之前发生的所有错误。catchcatch
2赞 ikegami 7/30/2022 #10

Promise 允许程序员编写比使用回调更简单、更具可读性的代码。


在程序中,我们想要串联执行一些步骤。

function f() {
   step_a();
   step_b();
   step_c();
   ...
}

每个步骤之间通常都有信息。

function f( x ) {
   const a = step_a( x );
   const b = step_b( a );
   const c = step_c( b );
   ...
}

其中一些步骤可能需要(相对)较长的时间,因此有时您希望将它们与其他步骤并行执行。一种方法是使用线程。另一个是异步编程。(这两种方法各有利弊,这里不讨论。在这里,我们谈论的是异步编程。

使用异步编程时,实现上述目标的简单方法是提供一个回调,一旦步骤完成,就会调用该回调。

// Each of step_* calls the provided function with its return value once complete.
function f() {
   step_a( x,
      function( a ) {
         step_b( a,
            function( b ) {
               step_c( b,
                  ...
               );
            },
         );
      },
   );
}

这很难读懂。Promise 提供了一种扁平化代码的方法。

// Each of step_* returns a promise.
function f( x ) {
   step_a( x )
   .then( step_b )
   .then( step_c )
   ...
}

返回的对象称为 promise,因为它表示函数的未来结果(即 promise 结果)(可以是值或异常)。

尽管 promise 有帮助,但使用 promise 仍然有点复杂。这就是进来的地方。在声明为 的函数中,可以使用 代替 。asyncawaitasyncawaitthen

// Each of step_* returns a promise.
async function f( x )
   const a = await step_a( x );
   const b = await step_b( a );
   const c = await step_c( b );
   ...
}

不可否认,这比使用回调更具可读性。