JavaScript是一种强大的编程语言,它具有闭合、一级函数和许多其他功能。JavaScript经常被用于异步编程,或者使用回调的编程方式。虽然功能强大,但这也会导致许多人现在所说的回调地狱。回调地狱也被亲切地称为 "末日金字塔",这要归功于许多层次的缩进,它们使你的代码看起来像一个难以阅读的金字塔。让我们回顾一下JavaScript异步编程从回调到async/await的演变,因为这对我们的Node.js编程有帮助。
JavaScript回调是用来做什么的?
JavaScript中的回调函数是可以作为一个参数传递给其他函数的函数。它们也被称为高阶函数,在被传递的函数中被调用(或执行)。回调是一种惯例,而不是JavaScript语言中的一种实际事物。当你需要在你的代码中做一些导致I/O或输入/输出操作的事情时,就会用到回调。像与数据库对话、下载照片或将文件写入磁盘都是I/O操作的例子。这些都是异步操作。换句话说,它们可能需要一些时间,或在未来发生。
让我们看一下这里的一个代码例子。
const getBlogPost = () => {
setTimeout(() => {
return {
title: 'JavaScript Callbacks'
}
}, 2000);
};
const blogpost = getBlogPost();
console.log(blogpost.title);
你认为运行这段代码后会发生什么?让我们来看看。
express-rest $node index.js
C:nodeexpress-restindex.js:21
console.log(blogpost.title);
^
TypeError: Cannot read property 'title' of undefined
at Object. (C:nodeexpress-restindex.js:21:22)
at Module._compile (module.js:643:30)
at Object.Module._extensions..js (module.js:654:10)
at Module.load (module.js:556:32)
at tryModuleLoad (module.js:499:12)
at Function.Module._load (module.js:491:3)
at Function.Module.runMain (module.js:684:10)
at startup (bootstrap_node.js:187:16)
at bootstrap_node.js:608:3
应用程序说它不能读取一个未定义的属性。但是为什么呢?JavaScript并没有等待异步操作的完成来继续推进代码的执行。它只是简单地运行程序的每一行,没有停顿。换句话说,它是无阻塞的。getBlogPost()函数立即运行并将blogpost 的值设置为未定义。这就是为什么在为JavaScript和Node编程时需要回调的原因。我们可以用回调来重写这个例子,这样我们就能得到我们想要的结果。
const getBlogPost = (callbackfunc) => {
setTimeout(() => {
callbackfunc({
title: 'JavaScript Callbacks'
});
}, 2000);
};
getBlogPost((blogpost) => {
console.log(blogpost.title);
})

这更符合我们所要的思路。我们启动程序,2秒钟后我们在屏幕上看到输出。
回调地狱
让我们想一想有可能导致回调地狱的情况。想象一下,在你的应用程序中,你想从数据库中获取一个用户。一旦你有了这个用户,你现在想得到这个用户的所有博客文章。一旦你有了博文,你还想获得该博文的评论。在同步方法中,这是很直接的。在异步的世界里,这可能会开始引起一些问题。这里突出显示的代码就是我们所说的 "厄运金字塔"。你也可以叫它回调地狱,但末日金字塔听起来很有杀伤力,哈哈!
getUser(1, (user) => {
getBlogPosts(user.name, (blogposts) => {
getComments(blogposts[0], (comments) => {
console.log(user, blogposts[0], comments);
})
})
});
function getUser(id, callbackfunc) {
setTimeout(() => {
console.log('Getting the user from the database...');
callbackfunc({
id: id,
name: 'Vegibit'
});
}, 1000);
}
function getBlogPosts(username, callbackfunc) {
setTimeout(() => {
console.log('Calling WordPress Rest API for posts');
callbackfunc(['Post1', 'post2', 'post3']);
}, 1000);
}
function getComments(post, callbackfunc) {
setTimeout(() => {
console.log('Calling WordPress Rest API for comments for ' + post);
callbackfunc(['comments for ' + post]);
}, 1000);
}
这段代码可以工作,但它并不那么容易阅读,也不容易看。更糟的是,如果你继续缩进更多层次的回调,你肯定会在代码中迷失方向,产生错误,并简单地创造出对你不利的反模式。

用命名的函数减少回调地狱
如果你不小心的话,JavaScript很容易让你的脚受伤。这种语言的一个特点就是匿名函数,它可以帮助你做到这一点。Getify以建议你总是命名你的函数而闻名,他说的很有道理。调试匿名函数几乎是不可能的。在任何情况下,让我们修改我们的代码,到目前为止,使用命名的函数,看看这对可读性有何帮助。在这里,我们重构了代码以使用命名函数。这样做比较好,但仍然不是很好。
getUser(1, getUserBlogPosts);
function getUser(id, callbackfunc) {
setTimeout(() => {
console.log('Getting the user from the database...');
callbackfunc({
id: id,
name: 'Vegibit'
});
}, 1000);
}
function getUserBlogPosts(user) {
getBlogPosts(user.name, getPosts);
}
function displayComments(comments) {
console.log(comments);
}
function getPosts(blogposts) {
getComments(blogposts[0], displayComments);
}
function getBlogPosts(username, callbackfunc) {
setTimeout(() => {
console.log('Calling WordPress Rest API for posts');
callbackfunc(['Post1', 'post2', 'post3']);
}, 1000);
}
function getComments(post, callbackfunc) {
setTimeout(() => {
console.log('Calling WordPress Rest API for comments for ' + post);
callbackfunc(['comments for ' + post]);
}, 1000);
}
JavaScript承诺是如何帮助的
许诺持有一个异步操作的最终结果。当一个异步操作完成后,它可以产生一个值或一个错误。一个承诺可以持有的三种状态是:待定、已完成或拒绝。下面是一个使用诺言进行API请求的虚构的例子。
const promise = new Promise((resolve, reject) => {
// Do some asynchronous tasks
// ...
setTimeout(() => {
let apiResponse = 1;
if (apiResponse == 1) {
resolve('It worked'); // pending => resolved, fulfilled
} else {
reject(new Error('Something went wrong')); // pending => rejected
}
}, 1000);
});
promise.then(result => console.log('Result:', result))
.catch(err => console.log('Error', err.message));
当API请求成功时(apiResponse = 1),一切都很好。

当API请求不成功时(apiResponse = 0),承诺链的.catch显示一个错误。

有了这样的理解,我们现在可以用一个返回的承诺来代替接受回调的异步函数。让我们看看怎么做。
承诺代替回调
在我们展示的回调地狱的示例代码中,我们有一个金字塔式的厄运结构。换句话说,我们有那种深度嵌套的问题,很难阅读。在这里,我们可以修改异步函数,使其现在返回一个承诺。由于这些异步函数返回一个承诺,我们就可以使用.then()和.catch()来消费这些承诺,从而消除了回调地狱的嵌套金字塔。注意厄运的金字塔现在被注释掉了,并在下面使用.then()链重新编写。还请注意,支持异步的函数不再接受回调作为参数,因为它们现在要返回一个Promise。希望大家能明白,在返回的Promise中,现在的异步代码是这样的(读取数据库,或进行API请求)。
// getUser(1, (user) => {
// getBlogPosts(user.name, (blogposts) => {
// getComments(blogposts[0], (comments) => {
// console.log(user, blogposts[0], comments);
// })
// })
// });
getUser(1)
.then(user => getBlogPosts(user.name))
.then(blogposts => getComments(blogposts[0]))
.then(comments => console.log(comments))
.catch(err => console.log('Error: ', err.message));
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Getting the user from the database...');
resolve({
id: id,
name: 'Vegibit'
});
}, 1000);
});
}
function getBlogPosts(username) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Calling WordPress Rest API for posts');
resolve(['Post1', 'post2', 'post3']);
}, 1000);
});
}
function getComments(post) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Calling WordPress Rest API for comments for ' + post);
resolve(['comments for ' + post]);
}, 1000);
});
}
异步等待句法糖
最后,你可以考虑使用回调地狱的最新解决方案,那就是async/await。async/await的目标是帮助软件开发者把异步代码写得像同步代码一样,或者以同步的风格来写。为了演示,下面是基于Promise和Async/Await方法的代码。请注意,Promise方法被注释掉了。
// getUser(1)
// .then(user => getBlogPosts(user.name))
// .then(blogposts => getComments(blogposts[0]))
// .then(comments => console.log(comments))
// .catch(err => console.log('Error: ', err.message));
async function displayComments() {
try {
const user = await getUser(1);
const blogposts = await getBlogPosts(user.name);
const comments = await getComments(blogposts[0]);
console.log(comments);
} catch (err) {
console.log('Error', err.message);
}
}
看起来这段代码在async/await方法下也能工作。

这里有一些关于async/await的关键事情需要理解。当你调用一个返回Promise的函数时,你要使用await关键字。这允许你把结果分配给一个常量或变量,就像你在同步操作中一样。例如,代码const user = await getUser(1); ,看起来非常漂亮,而且是同步的,但是await关键字给了我们一个指示,表明该函数正在返回一个承诺。这就方便了我们调用getBlogPosts(user.name) ,并将我们在前一行代码中得到的用户对象传入。任何时候你看到一个函数前的await关键字,你就知道它正在返回一个承诺。不过最棒的是,你现在可以把代码写得更像同步的风格。另一个启示是,由于你在函数中使用await ,你需要在该函数中使用async关键字。Async和Await是建立在Promises之上的,是一种拥有更好的语法的方式,就像ES6类是一种更好的原型和构造函数的语法。如果写异步代码让你头晕目眩,那么也许Async/Await正是你需要的。
Javascript回调VS承诺VS异步等待总结
当你在写异步代码时,很容易让自己陷入困境,尤其是当你已经写了很长时间的同步代码时。事实是,以一种干净简洁的方式使用回调是具有挑战性的。你可能会发现自己处于回调地狱的金字塔中,比你眨眼的速度还要快。