文章目录
前言什么是回调地狱里发生场景举个例子
解决方法里以 Promise 为例快速了解 Promise解决案例
最后
前言
好久没写面试题的文章了,今天这篇文章讲一讲我之前遇到一个面试题,当时在跟面试官聊了原型链、vue生命周期后,他问到了我什么是 Promise、然后又聊到了回调地狱,及其解决方法。当然了还有聊到async和await,这篇文章先不细聊async和await。在前端开发的面试中,异步编程是经常问到的知识点,所以Promise、async、await的等知识点要熟练掌握。接下来进入正题。
✅关于回调函数和异步编程的一些知识点,也可以看这篇文章:前端开发面试题—JavaScript回调函数与异步编程
什么是回调地狱
回调地狱指的是在编写异步逻辑时,多个回调函数嵌套在一起,形成复杂且难以维护的代码结构。在 JavaScript 中是常见的问题之一,因为 JavaScript 是单线程运行的,在执行异步操作时需要通过回调函数来处理完成后的处理逻辑,如果异步操作嵌套层数过多,就会导致代码结构复杂,可读性很差,而且难以维护。
里发生场景
多个异步操作的处理:比如读取文件、发送网络请求等。 嵌套的回调函数:有时我们需要在一个回调函数中再次执行另一个异步操作,这种情况下就会产生回调函数嵌套的问题。 异步函数的参数传递:在异步函数中,将结果传递到回调函数或者下一个异步函数中的方式是通过参数传递,这也可能导致复杂的嵌套问题。 DOM 事件监听:在网页开发中,我们经常需要添加 DOM 事件监听器,并在回调函数中执行其他异步操作,这也可能导致回调地狱的问题。
举个例子
假设通过接口 1 的返回值,去获取接口 2 的数据,然后,再通过接口 2 的返回值,获取接口 3 的数据。即每次请求接口数据时,都需要使用上一次的返回值。
var outnum = function (n, callback) {
setTimeout(function () {
console.log(n);
callback();
}, 1000);
};
outnum("1", function () {
outnum("2", function () {
outnum("3", function () {
console.log("0");
});
});
});
在上述代码中我们发现,虽然可以通过回调函数层层嵌套的形式达到最终数据请求的目的,但代码结构不甚明朗,可读性极差,这就是一个回调地狱。 如果你还想了解更多回调地狱的内容,可以看这个网站的内容:http://callbackhell.com
这个网站是一个介绍回调地狱(Callback Hell)的网站,它以幽默的方式演示了在 JavaScript 中出现回调地狱的情况,并提供了一些解决方案。(全英文)
解决方法
使用 Promise:Promise 是一种异步编程的解决方案,可以将异步操作转化为链式调用,以避免多层嵌套的回调函数。Promise可以使代码更容易维护和修改,并且可以通过 .then() 和 .catch() 等方法进行错误处理。 使用 async/await:async/await 是 ES2017 中引入的异步编程语法糖,可以将异步操作转化为同步风格的语法,让异步代码更易于编写和理解,并可以有效避免回调地狱的问题。 模块化:将复杂的异步函数拆分成多个模块,以减少代码的耦合度和复杂度,提高代码质量和可维护性。将各个模块之间的异步操作封装在不同的函数中,可以避免回调地狱的出现。 使用流程控制库:如 async.js、Q.js 等流程控制库,它们提供了一些方便处理异步流程的方法,可以简化异步操作的处理过程,并避免回调地狱的出现。
里以 Promise 为例
快速了解 Promise
Promise 是 JavaScript 的异步编程解决方案之一,它是一种更优雅的处理异步操作的方式。通过 Promise,可以更轻松地管理异步代码的执行流程和处理异步请求的结果。
Promise 是一个代表着未来某个时间点会产生结果的对象,而不是现在。Promise 可以有三种状态:pending(等待)、fulfilled(已完成)和 rejected(已拒绝)。Promise 一旦创建就不能取消。Promise 只有两种可能的结果:完成或拒绝,并且可以为这些结果设置对应的回调函数(.then() 和 .catch() 方法)。
由于 Promise 对象是一个构造函数,因此,必须通过实例化来生成,它的定义格式如下代码:
const promise = new Promise(function (resolve, reject) {
// 此处做一个异步的事情
});
接下来看一个例子:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const num = Math.floor(Math.random() * 10) + 1;
if (num > 5) {
resolve(num);
} else {
reject('Number is too small');
}
}, 1000);
});
promise.then((result) => {
console.log('The number is:', result);
}).catch((error) => {
console.error('An error occurred:', error);
});
在调用 Promise 的 .then() 方法时,可以为其传递一个完成时的回调函数;在调用 Promise 的 .catch() 方法时,可以为其传递一个拒绝时的回调函数。
以上代码中,Promise 在 1 秒钟后产生一个1-10的随机数,并使用 resolve 或 reject 来返回或拒绝该数值。使用 .then() 和 .catch() 方法添加了处理成功或失败的回调函数。
❗ 注意:Promise 可以链式调用多个 .then() 方法和一个 .catch() 方法,但每个 .then() 方法需要返回一个新的 Promise 对象。这允许我们将多个异步操作组合在一起,以更为复杂的方式编排它们的顺序和结果。
解决案例
以下是一个回调地狱的代码示例:
function getUser(userId, callback) {
setTimeout(() => {
const user = { id: userId, name: 'John' };
callback(user);
}, 1000);
}
function getCourse(courseId, callback) {
setTimeout(() => {
const course = { id: courseId, name: 'JavaScript' };
callback(course);
}, 1000);
}
function getEnrollment(userId, courseId, callback) {
setTimeout(() => {
getUser(userId, (user) => {
getCourse(courseId, (course) => {
const enrollment = { user, course };
callback(enrollment);
});
});
}, 1000);
}
getEnrollment(1, 2, (enrollment) => {
console.log(enrollment);
});
以上代码中,getEnrollment 函数依次调用了 getUser 和 getCourse 函数来获取用户和课程信息,并在嵌套的回调函数中重复执行这些操作。这会导致代码结构不够清晰、可读性较差。
下面是使用 Promise 重构后的代码:
function getUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = { id: userId, name: 'John' };
resolve(user);
}, 1000);
});
}
function getCourse(courseId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const course = { id: courseId, name: 'JavaScript' };
resolve(course);
}, 1000);
});
}
function getEnrollment(userId, courseId) {
return Promise.all([getUser(userId), getCourse(courseId)]).then(([user, course]) => {
const enrollment = { user, course };
return enrollment;
});
}
getEnrollment(1, 2).then((enrollment) => {
console.log(enrollment);
});
以上代码使用 Promise 将异步操作链式调用,并使用 Promise.all 方法来并行处理 getUser 和 getCourse 函数的结果。这大大简化了代码结构,使得代码更易读和维护。
以文章开头的回调地狱例子,我们也可以用 `Promise` 来进行修改,如下:
var outnum = function (order) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(order);
resolve();
}, 1000);
});
};
outnum("1")
.then(order=> {
return outnum("2");
})
.then(order=> {
return outnum("3");
})
.then(order=> {
console.log("0");
});
执行上述代码之后的效果与使用地狱式回调方式是完全一样的,但 Promise 对象实现的代码可读性更强,还可以很方便地取到异步执行后传递的参数值,因此,这种代码的实现方式,更适合在异步编程中使用。
❗ 当然还可以使用 async/await 来避免回调地狱。这篇文章就不详细演示了。
最后
回调地狱的最大问题在于它会使得代码阅读难度加大,代码结构不易懂,调试和维护难度大等问题,同时也容易引发错误。
为了避免回调地狱出现,可以使用 Promise、async/await 等技术来简化异步操作的处理方式,以更加清晰和优雅的方式实现异步操作。当然,依然要注意合理组织代码结构,保证代码可读性和可维护性,避免问题的发生。
推荐链接
发表评论