【问题标题】:Aren't promises just callbacks?承诺不只是回调吗?
【发布时间】:2014-04-27 16:51:04
【问题描述】:

我已经开发 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
          });
     });
});

哪个代码更多,可读性更低。我在这里没有任何收获,它也不是突然神奇地“平坦”。更不用说必须将事情转换为承诺。

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

【问题讨论】:

  • On-topic:有一篇关于 Html5Rocks 上的 Promises 的内容非常丰富的文章:html5rocks.com/en/tutorials/es6/promises
  • 仅供参考,您接受的答案是相同的旧列表,这些微不足道的好处根本不是承诺的重点,甚至没有说服我使用承诺:/。说服我使用 Promise 的是 Oscar 的回答中描述的 DSL 方面
  • @Esailija 很好,你的 leet 演讲说服了我。我接受了另一个答案,尽管我认为 Bergi 的答案也提出了一些非常好的(和不同的)观点。
  • @Esailija “说服我使用承诺的是奥斯卡回答中描述的 DSL 方面”
  • @monsto:DSL:领域特定语言,一种专门设计用于系统特定子集的语言(例如,用于与数据库对话的 SQL 或 ORM,用于查找模式的正则表达式等)。在这种情况下,“DSL”是 Promise 的 API,如果你按照 Oscar 的方式构建代码,它几乎就像补充 JavaScript 以解决异步操作的特定上下文的语法糖。 Promise 创造了一些惯用语,将它们变成了几乎一种旨在让程序员更容易掌握这种结构的难以捉摸的思维流程的语言。

标签: javascript callback promise q bluebird


【解决方案1】:

Promise 不是回调。承诺代表异步操作的未来结果。当然,按照你的方式编写它们,你得到的好处很少。但是如果你按照它们的用途来编写它们,你可以用一种类似于同步代码的方式来编写异步代码,并且更容易理解:

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

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

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

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

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

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

希望你现在以全新的眼光看待 Promise。

【讨论】:

  • 他们真的不应该把它命名为“承诺”。 “未来”至少要好 100 倍。
  • @Pacerier 因为 Future 没有被 jQuery 污染?
  • 替代模式(取决于需要什么:api().then(api2).then(api3).then(doWork); 也就是说,如果 api2/api3 函数从最后一步获取输入, 并自己返回新的 Promise,它们可以直接链接起来而无需额外的包装。也就是说,它们是 compose。
  • 如果api2api3有异步操作怎么办?最后一个 .then 只有在这些异步操作完成后才会被调用吗?
  • 使用promise就没有缺点吗?如果是这样,我应该尽可能使用承诺而不是回调吗?
【解决方案2】:

是的,Promise 是异步回调。它们不能做任何回调不能做的事情,而且你在异步和普通回调中面临同样的问题。

然而,Promise更多不仅仅是回调。它们是一个非常强大的抽象,允许使用更简洁、更好的函数式代码以及不易出错的样板。

那么主要的想法是什么?

Promises 是表示单个(异步)计算结果的对象。他们resolve to that result 只有一次。这意味着几件事:

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

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

Promises are chainablemonadicif you want):

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

听起来很复杂?是时候写一个代码示例了。

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 */);

如果看到这些方法的代码有助于理解,here's a most basic promise lib in a few lines

承诺有什么大惊小怪的?

Promise 抽象允许更好的函数组合性。例如,在用于链接的 then 旁边,all 函数为多个并行等待的 Promise 的组合结果创建一个 Promise。

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

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

对于好的 promise 库来说,这实际上是微不足道的,请参阅 How do I convert an existing callback API to promises?

【讨论】:

  • 嗨,Bergi,您有什么有趣的事情要添加到这个 SO 问题中吗? stackoverflow.com/questions/22724883/…
  • @Sebastien:我对 Scala 了解不多(目前),我只能重复 Benjamin 所说的 :-)
  • 请注意:您不能使用.then(console.log),因为console.log 取决于控制台上下文。这样会导致非法调用错误。使用console.log.bind(console)x => console.log(x) 绑定上下文。
  • @hege_hegedus:有些环境已经绑定了console 方法。当然,我只是说两个嵌套具有完全相同的行为,并不是说它们中的任何一个都可以工作:-P
  • 太棒了。这就是我所需要的:更少的代码和更多的解释。谢谢。
【解决方案3】:

除了已经确定的答案之外,使用 ES6 箭头函数,Promises 从一个适度闪亮的小蓝矮星变成一个红巨星。那即将坍缩成超新星:

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

正如oligofren 指出的那样,在 api 调用之间没有参数,您根本不需要匿名包装函数:

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

最后,如果你想达到超大质量黑洞级别,可以等待 Promise:

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

    return api3Result;
}

【讨论】:

  • "带有 ES6 箭头函数的 Promises 从一颗微光闪闪的蓝色小星直接变成了一颗红巨星。那即将坍塌成一颗超新星" 翻译:将 ES6 箭头函数与 Promise 结合起来真是太棒了:)
  • 这让 Promises 听起来像是一场宇宙灾难,我认为这不是你的本意。
  • 如果你没有使用apiX方法中的参数,你还不如完全跳过箭头函数:api().then(api2).then(api3).then(r3 => console.log(r3))
  • @MichaelMcGinnis -- Promises 对沉闷的回调地狱的有益影响就像在太空黑暗角落爆炸的超新星。
  • 我知道你的意思是诗意的,但承诺与“超新星”相去甚远。 Breaking monadic law 或缺乏对更强大用例的支持,例如取消或返回多个值。
【解决方案4】:

除了上面的精彩答案,还可以补充2点:

1.语义差异:

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

相反,回调 处理事件。所以,如果你感兴趣的事件在回调注册之前发生了,回调就不会被调用。

2。控制反转

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

请参阅The Javascript Event loop 以获得解释。

使用Promises,控制权在调用程序。 .then() 方法可以在任何时候被调用如果我们存储了 promise 对象。

【讨论】:

  • 我不知道为什么,但这似乎是一个更好的答案。
  • 很好,这 ->“有了 Promises,控制权就在调用程序中。如果我们存储了 Promise 对象,可以随时调用 .then() 方法。”
【解决方案5】:

除了其他答案之外,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
});

【讨论】:

    【解决方案6】:

    不,一点也不。

    Callbacks 只是JavaScript 中的函数,它们将在另一个函数执行完成后被调用并执行。那么它是怎么发生的呢?

    实际上,在 JavaScript 中,函数本身被视为对象,因此与所有其他对象一样,甚至函数也可以作为参数发送给其他 functions。可以想到的最常见和通用的用例是 JavaScript 中的 setTimeout() 函数。

    Promises 与处理回调相比,只不过是一种更即兴的处理和结构化异步代码的方法。

    Promise 在构造函数中接收两个回调:resolve 和 reject。 Promise 中的这些回调为我们提供了对错误处理和成功案例的细粒度控制。当promise执行成功时使用resolve回调,错误情况使用reject回调处理。

    【讨论】:

      【解决方案7】:

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

      【讨论】:

        【解决方案8】:

        没有承诺只是回调的包装

        示例 您可以将 javascript 原生 Promise 与 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
        

        【讨论】:

          【解决方案9】:

          JavaScript Promise 实际上使用回调函数来确定在 Promise 被解决或拒绝后要做什么,因此两者并没有根本的不同。 Promises 背后的主要思想是采用回调 - 尤其是嵌套回调,您希望在其中执行某种操作,但它会更具可读性。

          【讨论】:

            【解决方案10】:

            Promise 概览:

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

            promise 总是具有以下 3 种状态:

            1. pending:每个 promise 的起始状态,既不履行也不拒绝。
            2. 已完成:操作已成功完成。
            3. rejected:操作失败。

            一个未决的承诺可以用一个值来解决/完成或拒绝。然后调用以下将回调作为参数的处理程序方法:

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

            虽然上面的方法skill get callback arguments,但它们远胜于使用 这里只有回调是一个可以澄清很多的例子:

            示例

            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 函数创建一个承诺,根据 1 秒后的随机 Nr 解决或拒绝该承诺
            • 如果 promise 已解决,则调用第一个 then 方法并将已解决的值作为回调的参数传入
            • 如果 promise 被拒绝,则调用第一个 catch 方法并将拒绝的值作为参数传入
            • catchthen 方法返回承诺,这就是我们可以链接它们的原因。它们将任何返回值包装在Promise.resolve 中,并将任何抛出的值(使用throw 关键字)包装在Promise.reject 中。因此,任何返回的值都会被转换为一个 Promise,并且我们可以在这个 Promise 上再次调用一个处理函数。
            • Promise 链为我们提供了比嵌套回调更精细的控制和更好的概览。例如,catch 方法处理在 catch 处理程序之前发生的所有错误。

            【讨论】:

              猜你喜欢
              • 2014-09-27
              • 2016-06-15
              相关资源
              最近更新 更多