承诺不就是回调函数吗?

507

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

这段代码更冗长,不易读懂。我在这里没有获得任何好处,它也不会神奇地变得“扁平”。更别提必须将事物转换为promises。

那么,这里关于promises有什么大惊小怪的呢?


12
有一篇非常信息丰富的关于Promises的文章在Html5Rocks上:http://www.html5rocks.com/en/tutorials/es6/promises/ - ComFreek
2
顺便说一下,你接受的答案只是那些微不足道的好处的老套路清单,这些根本不是承诺的重点,甚至没有让我相信使用承诺 :/。让我相信使用承诺的是 Oscar 回答中描述的 DSL 方面。 - Esailija
@Esailija,很好,你的“1337”语言说服了我。虽然我认为Bergi的答案也提出了一些非常好(而且不同)的观点,但我已经接受了其他答案。 - Benjamin Gruenbaum
1
@Esailija:“让我相信使用Promise的是Oscar回答中所描述的DSL方面。”<<“DSL”是什么?你所提到的“DSL方面”是什么? - monsto
3
DSL:领域特定语言。它是专门设计用于系统子集中的语言,例如SQL或ORM用于与数据库通信,正则表达式用于查找模式等。在这个上下文中,“DSL”是Promise的API,如果像Oscar一样结构化代码,则几乎像是一种语法糖,可以补充JavaScript以应对异步操作的特定上下文。Promises创造了一些惯用语法,使它们几乎成为一种语言,旨在让程序员更容易掌握此类结构的相对难懂的思维流程。 - Michael Ekoka
11个回答

710

Promise不是回调函数。一个Promise代表一项异步操作的未来结果。当然,如果像你所写的那样,你得到的好处很少。但如果按照它们应该使用的方式编写它们,您可以以类似于同步代码的方式编写异步代码,并且更易于理解:

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

当然,代码量并没有减少,但可读性提高了许多。

但这还不是全部。让我们探索真正的好处:如果您想要检查任何步骤中的错误,使用回调将会很麻烦,但使用 Promises 就非常简单:

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个对apiapi2api3的调用可以同时运行(例如,如果它们是AJAX调用),但您需要等待这三个调用完成后再继续下一步,该怎么办呢?没有 Promise,您应该要创建某种计数器。有了 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。


146
取名为“Promise”真的不应该,至少“Future”的效果会好100倍。 - Pacerier
14
因为未来没有被 jQuery 污染? - Esailija
12
根据需要选择交替模式:api().then(api2).then(api3).then(doWork)。如果api2/api3函数从上一步骤中获取输入并返回新的承诺,则它们可以直接链接而不需要额外的包装,即它们可以组合在一起。 - Dtipson
1
如果api2api3中有异步操作,那么最后一个.then只有在这些异步操作完成后才会被调用吗? - NiCk Newman
2
我发现这个答案是如何使用 Promise 的最好示例之一。所以,我不明白为什么会被严重地踩 (-6 至今)。 - Déjà vu
显示剩余5条评论

198
是的,Promises是异步回调。它们不能做任何回调所不能做的事情,并且您面临与普通回调一样的异步问题。
然而,Promises不仅仅是回调函数。它们是非常强大的抽象,可以使用更少容易出错的样板代码编写更清洁和更好的功能代码。
那么主要思想是什么呢?
Promises是表示单个(异步)计算结果的对象。它们只能解决一次。这意味着几件事:
Promises实现了观察者模式:
  • 您不需要在任务完成之前知道将使用该值的回调。
  • 您可以轻松地返回一个Promise对象,而不是期望回调作为函数的参数。
  • Promise将存储该值,您可以在需要时透明地添加回调。当结果可用时,将调用它。“透明度”意味着当您拥有一个Promise并向其添加回调时,无论结果是否已到达,对于您的代码都没有影响- API和合同相同,大大简化了缓存/记忆。
  • 您可以轻松添加多个回调。

Promise是可链接的单子如果你想要):

  • 如果您需要转换一个 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 */);

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

promise 有什么大惊小怪的?

Promise 抽象允许更好地组合函数。例如,除了用于链接的 thenall 函数创建一个承诺,用于多个并行等待的承诺的组合结果。

最后但并非最不重要的是,Promise 带有集成的错误处理。计算的结果可能是承诺被实现了一个值,或者它被拒绝了一个原因。所有组合函数都自动处理这个,并在承诺链中传播错误,因此您不需要在每个地方显式关注它 - 与纯回调实现相反。最终,您可以为所有发生的异常添加专用的错误回调。

更不用说必须将事物转换为 promises。

使用良好的 promise 库实际上很容易,参见 如何将现有的回调 API 转换为 promises?


1
嗨Bergi,你有什么有趣的东西可以添加到这个SO问题吗?https://dev59.com/z2Eh5IYBdhLWcg3wKgug - Sebastien Lorber
1
@Sebastien:我对Scala还不是很了解,我只能重复Benjamin说的话 :-) - Bergi
4
只是一个小注释:你不能使用.then(console.log),因为console.log依赖于控制台上下文。这样会导致非法调用错误。请使用console.log.bind(console)x => console.log(x)来绑定上下文。 - Tamas Hegedus
3
有一些环境已经将console方法绑定了。当然,我只是说两种嵌套的行为完全相同,而不是任何一种都可以工作 :-P - Bergi
1
太好了。这正是我所需要的:更少的代码,更多的解释。谢谢。 - Adam Patterson
显示剩余9条评论

27

除了已经建立的答案之外,使用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))

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

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

    return api3Result;
}

8
这让“承诺”听起来像是一场宇宙大灾难,我不认为这是你的本意。 - Michael McGinnis
1
如果您在apiX方法中没有使用参数,那么最好完全跳过箭头函数: api().then(api2).then(api3).then(r3 => console.log(r3)) - oligofren
@MichaelMcGinnis -- Promise对于一个枯燥的回调地狱所带来的益处,就像黑暗角落中爆炸的超新星一样。 - John Weisz
我知道你的意思是诗意的,但承诺距离“超新星”相当遥远。违反单子法则或缺乏更强大的用例支持,例如取消或返回多个值。 - Dmitri Zaitsev

24
除了上面的精彩答案,还可以补充两点:
1. 语义差异:
Promise可能在创建时已经解决。这意味着它们保证条件而不是事件。如果它们已经被解决,传递给它的解决函数仍然会被调用。
相反,回调处理事件。因此,如果您感兴趣的事件在回调注册之前发生,回调将不会被调用。
2. 控制反转
回调涉及控制反转。当您向任何API注册回调函数时,Javascript运行时会存储回调函数,并在准备好运行时从事件循环中调用它。
请参阅JavaScript事件循环以获得解释。
对于Promises,控制权归调用程序所有。如果我们存储Promise对象,则随时可以调用.then()方法。

1
我不知道为什么,但这似乎是一个更好的答案。 - radiantshaw
不错,使用 Promise,控制权在调用程序中。如果我们存储了 Promise 对象,则可以随时调用 .then() 方法。 - HopeKing
这应该是正确的答案,特别是语义差异部分。我心里有一段时间了,但无法用言语表达,但这是承诺和回调之间核心的区别。 - Ramy Farid

15

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

5
承诺和回调都是促进异步编程的编程习惯,它们不同。使用协程或生成器以async/await方式进行编程并返回承诺可以被认为是第三种这样的习惯用法。这里比较了不同编程语言(包括Javascript)中这些习惯用法:https://github.com/KjellSchubert/promise-future-task

5
No, 没有。 回调函数只是在JavaScript中被调用并执行另一个函数结束后执行的函数。它是如何实现的?
实际上,在JavaScript中,函数本身就被视为对象,因此像所有其他对象一样,甚至可以将函数作为参数发送到其他函数。最常见和通用的用例是JavaScript中的setTimeout()函数。 Promise只是相对于使用回调函数处理和结构化异步代码的更加改进的方法。
构造函数中的 Promise 接收两个回调函数:resolve 和 reject。这些回调函数在 Promise 内部提供了对错误处理和成功情况的细粒度控制。当 Promise 的执行成功时,使用 resolve 回调函数,而使用 reject 回调函数来处理错误情况。

3

承诺只是回调函数的包装器

例如,您可以在Node.js中使用JavaScript本机承诺

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

2

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仍然有点复杂。这就是asyncawait发挥作用的地方。在声明为async的函数中,可以使用await代替then
// 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 );
   ...
}

这无疑比使用回调函数更易读。


2

Promise概述:

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

一个promise始终具有以下3种状态之一:

  1. pending:每个promise的起始状态,既未完成也未被拒绝。
  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对象,根据1秒后生成的随机数来决定该Promise是resolved还是rejected。
  • 如果Promise被resolved,那么第一个then方法将被调用,并将resolved的值作为回调函数的参数传递。
  • 如果Promise被rejected,那么第一个catch方法将被调用,并将rejected的值作为参数传递。
  • catchthen方法返回的都是Promise对象,因此我们可以将它们链接在一起。它们使用Promise.resolve包装任何返回值,使用Promise.reject包装任何抛出的值(使用throw关键字)。所以任何返回的值都会被转换成一个Promise对象,我们可以在这个Promise对象上再次调用处理函数。
  • Promise链比嵌套回调更加细致的控制和更好的概览。例如catch方法处理在其之前发生的所有错误。

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