不只承诺回调吗?


430

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

这是更多的代码和更少的可读性。我在这里什么都没得到,也不是突然变得神奇地“平坦”。更不用说必须将事情变成诺言。

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


11
主题:关于Html5Rocks上的Promises的内容非常丰富:html5rocks.com/en/tutorials/es6/promises
ComFreek 2014年

2
Fyi,您接受的答案是琐碎的好处的老清单,这些清单根本不是承诺的重点,甚至没有说服我使用诺言:/。正如奥斯卡的回答所述,说服我使用诺言的是DSL方面
Esailija 2014年

@Esailija很好,你的leet说服了我。我接受了另一个答案,尽管我认为Bergi的答案也提出了一些非常好的(和不同的)观点。
Benjamin Gruenbaum 2014年

@Esailija“说服我遵守诺言的是奥斯卡回答中所描述的DSL方面” <<什么是“ DSL”?您指的是“ DSL方面”?
monsto

1
@monsto:DSL:领域特定语言,一种专门设计用于系统的特定子集的语言(例如,SQL或ORM与数据库对话,正则表达式以查找模式等)。在这种情况下,“ DSL”是Promise的API,如果您按照Oscar的方式来构造代码,则几乎就像语法糖,它补充了JavaScript以解决异步操作的特定上下文。承诺创建了一些成语,使它们成为几乎一种旨在使程序员更容易掌握这种结构的难以捉摸的思维的语言。
Michael Ekoka,

Answers:


631

承诺不是回调。许诺代表异步操作未来结果。当然,以您的方式编写它们,您会获得很少的收益。但是,如果按照使用它们的方式来编写它们,则可以以类似于同步代码的方式编写异步代码,并且更容易遵循:

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

更妙的是:如果这些3调用什么apiapi2api3可以同时运行(例如,如果他们是AJAX调用),但你需要等待三个?没有承诺,您应该必须创建某种计数器。使用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.
});

希望您现在看到一个崭新的承诺。


124
他们真的不应该将其命名为“ Promise”。“未来”至少要好100倍。
Pacerier,2014年

12
@Pacerier,因为Future没有被jQuery污染吗?
Esailija 2015年

5
备用模式(取决于所需的内容:api()。then(api2).then(api3).then(doWork);也就是说,如果api2 / api3函数从最后一步获取输入,并自己返回新的Promise,则它们可以不用多余的包裹就可以被链接起来。也就是说,它们组成了
Dtipson

1
如果有异步的operatings api2api3.then这些异步操作完成后,才会调用最后一个吗?
NiCk Newman

8
你为什么给我加上标签?我只是固定了一下语法。我不是JS专家。:)
Scott Arciszewski

169

是的,Promise是异步回调。他们无法做回调不能做的任何事情,并且异步处理和普通回调一样会遇到同样的问题。

然而,承诺是不仅仅是回调。它们是非常强大的抽象,它允许更干净,更好的功能代码,并且不易出错。

那么主要思想是什么?

承诺是表示单个(异步)计算结果的对象。他们决心解决这个问题一次。这意味着几件事:

承诺实现观察者模式:

  • 您无需在任务完成之前就知道将使用该值的回调。
  • 不用期望将回调作为函数的参数,而是可以轻松实现 return实现Promise对象
  • 承诺将存储价值,您可以透明地在需要时添加回调。结果可用时将调用它。“透明度”表示当您有一个诺言并向其添加回调时,结果是否到来对您的代码没有影响-API和协定相同,从而大大简化了缓存/存储。
  • 您可以轻松添加多个回调

承诺是可链接一元如果你想):

  • 如果需要转换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 lib

关于承诺的大惊小怪是什么?

Promise抽象允许更好的功能可组合性。例如,then对于链接,该all函数为多个并行等待的promise的组合结果创建一个promise。

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

更不用说必须将事情变成诺言。

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


嗨,Bergi,您是否有任何有趣的事情要添加到SO问题中?stackoverflow.com/questions/22724883/…–
塞巴斯蒂安·洛伯

1
@Sebastien:我对Scala还是不太了解,但我只能重复本杰明说的话:-)
Bergi 2014年

3
请注意:您不能使用.then(console.log),因为console.log取决于控制台上下文。这样会导致非法调用错误。使用console.log.bind(console)x => console.log(x)绑定上下文。
塔玛斯·赫格杜斯

3
@hege_hegedus:在某些环境中console已经绑定了方法。当然,我只说过两个嵌套都具有完全相同的行为,而不是它们中的任何一个都可以工作:-P
Bergi 2015年

1
那很棒。这就是我需要的:更少的代码和更多的解释。谢谢。
亚当·帕特森

21

除了已经确定的答案之外,借助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))

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

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

    return api3Result;
}

9
“具有ES6箭头功能,Promise会从适度发光的小蓝星直接变成红色巨人。这将崩溃成为超新星”转换:将ES6箭头功能与Promises结合是非常棒的:)
user3344977

3
这使Promises听起来像是一场宇宙灾难,我认为这不是你的意图。
Michael McGinnis

如果未在apiX方法中使用参数,则最好完全跳过箭头功能:api().then(api2).then(api3).then(r3 => console.log(r3))
oligofren

@MichaelMcGinnis-Promises对沉闷的回调地狱的有益影响就像是太空暗角中爆炸的超新星。
约翰·威兹

我知道您的意思是诗意的,但诺言与“超新星”相去甚远。想到了违反单子法或缺乏对更强大的用例(例如取消或返回多个值)的支持。
Dmitri Zaitsev

15

除了上述真棒答案外,还可以添加2点:

1.语义差异:

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

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

2.控制权倒置

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

请参阅Javascript事件循环以获取解释。

通过Promises,控制权归调用程序所有。如果我们存储promise对象,则可以随时调用.then()方法


1
我不知道为什么,但这似乎是一个更好的答案。
radiantshaw

13

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

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

// Parallel:
Promise.all([
    api1(),
    api2(),
    api3()
]).then(([r1, r2, r3]) => {
    // Done
});


4

一点都不。

回调只是JavaScript中的函数中的另一个函数的执行完成之后,这些函数将被调用然后执行。那怎么发生的呢?

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

承诺与回调相比,只是处理和构造异步代码的一种更为简易的方法。

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


2

没有承诺只是回调的包装

示例您可以将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

0

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


0

承诺概述:

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

一个承诺始终具有以下三种状态:

  1. 待定:每一个诺言的开始状态,既未实现也不被拒绝。
  2. 完成:操作成功完成。
  3. 拒绝:操作失败。

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

  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函数创建一个承诺,该承诺将在1秒后根据随机Nr来解决或拒绝
  • 如果承诺被解决,then则调用第一个方法,并将解决的值作为回调的参数传入
  • 如果承诺被拒绝,catch则调用第一个方法,并将拒绝的值作为参数传递
  • catchthen方法返回的承诺,这就是为什么我们可以把它们连。他们在中包装任何返回值,在中包装任何Promise.resolve抛出的值(使用throw关键字)Promise.reject。因此,返回的任何值都将转换为一个Promise,并且在此Promise上,我们可以再次调用处理程序函数。
  • 与嵌套回调相比,Promise链为我们提供了更好的控制和更好的概览。例如,该catch方法处理在catch处理程序之前发生的所有错误。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.