提问人:Benjamin Gruenbaum 提问时间:3/21/2014 最后编辑:Benjamin Gruenbaum 更新时间:3/27/2023 访问量:127471
承诺不就是回调吗?
Aren't promises just callbacks?
问:
我开发 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
});
});
});
这更多的代码和更低的可读性。我在这里什么也没得到,它也不是突然神奇地“平坦”了。更不用说必须将事情转化为承诺了。
那么,这里的承诺有什么大惊小怪的呢?
答:
承诺不是回调。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 符号,是另一回事,而且非常简洁:api
api2
api3
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.
});
希望你现在以新的眼光看待应许。
评论
api2
api3
.then
是的,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。then
all
最后但并非最不重要的一点是,Promise 带有集成的错误处理。计算的结果可能是,要么用一个值实现承诺,要么用一个理由拒绝它。所有组合函数都会自动处理这个问题,并在 promise 链中传播错误,因此您不需要在任何地方显式地关心它 - 与普通回调实现相反。最后,您可以为所有发生的异常添加专用的错误回调。
更不用说必须将事情转化为承诺了。
对于好的 promise 库来说,这实际上是微不足道的,请参阅如何将现有的回调 API 转换为 promise?
评论
.then(console.log)
console.log.bind(console)
x => console.log(x)
console
Promise 不是回调,两者都是促进异步编程的编程习惯用语。使用返回 promise 的协程或生成器使用异步/等待风格的编程可以被认为是第三个这样的习语。这些习语在不同编程语言(包括 Javascript)中的比较如下: https://github.com/KjellSchubert/promise-future-task
没有承诺只是回调的包装器
例 您可以将 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
除了已经确定的答案之外,通过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;
}
评论
apiX
api().then(api2).then(api3).then(r3 => console.log(r3))
除了其他答案之外,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
});
除了上面令人敬畏的答案外,还可以添加 2 点:
1.语义差异:
承诺可能在创建时就已经解决了。这意味着它们保证的是条件而不是事件。如果它们已被解析,则仍会调用传递给它的已解析函数。
相反,回调处理事件。因此,如果您感兴趣的事件发生在注册回调之前,则不会调用回调。
2. 控制反转
回调涉及控制反转。当您向任何 API 注册回调函数时,Javascript 运行时会存储回调函数,并在准备好运行后从事件循环中调用它。
有关说明,请参阅 Javascript 事件循环。
使用 Promises,控制权属于调用程序。如果我们存储 promise 对象,则可以随时调用 .then() 方法。
评论
JavaScript Promise 实际上使用回调函数来确定在 Promise 被解析或拒绝后要做什么,因此两者没有根本区别。Promise 背后的主要思想是接受回调 - 尤其是嵌套回调,你想执行某种操作,但它会更具可读性。
承诺概述:
在 JS 中,我们可以将异步操作(例如数据库调用、AJAX 调用)包装在 promise 中。通常,我们希望对检索到的数据运行一些额外的逻辑。JS promise 具有处理异步操作结果的处理程序函数。处理程序函数甚至可以在其中具有其他异步操作,这些操作可能依赖于先前异步操作的值。
承诺始终具有以下 3 种状态:
- 待定:每个承诺的起始状态,既未兑现也不被拒绝。
- fulfilled:操作已成功完成。
- rejected:操作失败。
可以使用值解析/完成或拒绝挂起的 promise。然后调用以下将回调作为参数的处理程序方法:
Promise.prototype.then()
:当 promise 解析时,将调用此函数的回调参数。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 上,我们可以再次调用处理函数。
catch
then
Promise.resolve
throw
Promise.reject
- 与嵌套回调相比,Promise 链为我们提供了更精细的控制和更好的概览。例如,该方法处理在处理程序之前发生的所有错误。
catch
catch
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 仍然有点复杂。这就是进来的地方。在声明为 的函数中,可以使用 代替 。async
await
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 );
...
}
不可否认,这比使用回调更具可读性。
评论