本文详细解析JavaScript中异步编程面临的回调地狱问题,并深入介绍Promise如何优化这一过程。内容涵盖Promise的三种状态、创建方法、实例方法(then、catch、finally)及静态方法(resolve、reject、race、all、allSettled),并通过图片加载示例展示链式调用与并行处理的实际应用。

为什么需要Promise?
在JavaScript中处理异步操作时,开发者往往无法预知任务何时完成,但必须在任务结束后执行后续逻辑。传统方案是依赖回调函数,下面通过一个常见场景进行说明。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
function loadImage(imgUrl, callback) {
const img = document.createElement("img");
img.onload = function () {
callback(this);
};
img.src = imgUrl;
}
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
(img) => document.body.appendChild(img)
);
</script>
</html>上述示例展示了图片加载完成后将其插入body元素并显示在页面上的过程。然而,若需在当前图片加载完毕后继续加载其他图片,则必须在回调函数内部再次调用loadImage方法。
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
}
);
}
);若需继续增加第三张图片呢?
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg",
(img) => {
document.body.appendChild(img);
}
);
}
);
}
);按上述逻辑继续添加图片,会导致每层回调函数中都需要调用loadImage,从而形成难以维护的“回调地狱”。

Promise
定义
Promise提供了一种更直观且可靠的异步编程解决方案,优于传统的异步处理方案。
状态
Promise对象包含三种状态。
- pending:执行中,即Promise创建后的初始状态。
- fulfilled:执行成功,即异步操作成功并取得预期结果后的状态。
- rejected:执行失败,即异步操作失败且未取得预期结果后的状态。
创建方法
const promise = new Promise((resolve, reject) => {})Promise构造函数接收一个执行器函数,该函数接收两个回调函数作为参数。当执行器产生结果后,会调用这两个回调函数之一。
- resolve:在函数执行成功时调用,并将执行器获取的结果作为实参传递给它,调用形式如resolve(获取到的结果)。
- reject:在函数执行失败时调用,并将具体的失败原因传递给它,调用形式如reject(失败原因)。
注意:resolve和reject两个回调函数在Promise类内部已定义好函数体,若想了解具体实现,可搜索Promise的源码。
const promise = new Promise((resolve, reject) => {
/* 做一些需要时间的事,之后调用可能会resolve 也可能会reject */
setTimeout(() => {
const random = Math.random()
console.log(random)
if (random > 0.5) {
resolve('success')
} else {
reject('fail')
}
}, 500)
})
console.log(promise)在浏览器控制台执行上述代码。

可以看到,初始时promise的状态为pending状态,500ms后promise的状态转变为rejected。
状态转换
当执行器获取到结果并调用resolve或reject中的一个时,整个promise对象的状态会发生变化。

该状态转换过程是不可逆的,一旦转换发生,状态将不再改变。
实例方法
promise对象包含两个用于消费执行器结果的函数:then和catch,而finally用于执行清理工作。
then
then函数接收两个函数作为参数。当执行器传递的结果状态为fulfilled时,第一个函数参数会接收执行器传递的结果作为参数并执行;当执行器传递的结果状态为rejected时,第二个函数参数会接收执行器传递的结果作为参数并执行。
const promise = new Promise((resolve, reject) => {
/* 做一些需要时间的事,之后调用可能会resolve 也可能会reject */
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve("success");
} else {
reject("fail");
}
}, 500);
});
console.log(promise);
promise.then(
(res) => console.log("resolved: ", res), // 生成的随机数大于0.5,则会执行这个函数
(err) => console.error("rejected: ", err) // 生成的随机数小于0.5,则会执行这个函数
);可多次尝试执行上述代码,注意控制台打印信息,验证是否符合上述结论。
若只对成功情况感兴趣,可仅向then函数提供一个函数参数。
const promise = new Promise((resolve) => setTimeout(() => resolve("done"), 1000))
promise.then(console.log) // 1秒后打印done若只对错误情况感兴趣,可向then的第一个参数提供null,在第二个参数提供具体函数。
const promise = new Promise((resolve, reject) =>
setTimeout(() => reject("fail"), 1000)
);
promise.then(null, console.log); // 1秒后打印failcatch
catch函数接收一个函数作为参数,当执行器传递的结果状态为rejected时,该函数才会被调用。
const promise = new Promise((reject) => setTimeout(() => reject("fail"), 500)).catch(
console.log
)
promise.catch(console.log)可能有同学会发现,传递给catch的参数与传递给then的第二个参数形式相似,这两种方式有何差异?答案是没有,then(null, errorHandler)和catch(errorHandler)这两种用法都能达到一样的效果,都能消费执行器执行失败时传递的原因。
finally
常规try-catch语句包含finally语句,在promise中也有finally,它接收一个函数作为参数,无论执行器得到的结果状态是fulfilled还是rejected,该函数参数一定会被执行。finally的目的是执行清理动作,例如在请求完成后停止显示loading图标。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve("success");
} else {
reject("fail");
}
}, 500);
});
promise
.finally((res) => {
console.log("======res======", res); // 打印undefined
console.log("task is done, do something");
})
.then(console.log, console.error); // 打印success或者fail通过打印结果可确定两点。
- 传递给finally的函数不会接收执行器处理后的结果。
- finally函数不参与对执行器产生结果的消费,将执行器产生的结果传递给后续程序进行消费。
手动实现finally函数,对上述两点会有更清晰的理解。
Promise.prototype._finally = function (callback) {
return this.then(
(res) => {
callback();
return res;
},
(err) => {
callback();
throw err;
}
);
};链式调用
前文介绍的then、catch以及finally函数在调用后均会返回promise对象,进而可再次调用then、catch以及finally,实现链式调用。
const promise = new Promise((resolve) => {
setTimeout(() => resolve(1), 1000);
});
promise
.then((res) => {
console.log(res); // 1
return res * 2;
})
.then((res) => {
console.log(res); // 2
return res * 2;
})
.then((res) => {
console.log(res); // 4
return res * 2;
})
.then((res) => {
console.log(res); // 8
});使用链式调用方式优化先前加载图片的代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
function loadImage(imgUrl) {
return new Promise((resolve) => {
const img = document.createElement("img");
img.onload = function () {
resolve(this);
};
img.src = imgUrl;
});
}
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg"
)
.then((img) => {
document.body.appendChild(img);
return loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg"
);
})
.then((img) => {
document.body.appendChild(img);
return loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg"
);
})
.then((img) => {
document.body.appendChild(img);
});
</script>
</html>注意:刚刚接触promise的同学不要犯下面这种错误,下面这种代码也是回调地狱的例子。
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg"
).then((img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg"
).then((img) => {
document.body.appendChild(img);
loadImage(
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg"
).then((img) => {
document.body.appendChild(img);
});
});
});静态方法
Promise.resolve
用于生成状态为fulfilled的promise对象,使用方式如下。
const promise = Promise.resolve(1) // 生成值为1的promise对象代码实现如下。
Promise.resolve2 = function (value) {
return new Promise((resolve) => {
resolve(value);
});
};Promise.reject
用于生成状态为rejected的promise对象,使用方式如下。
const promise = Promise.reject('fail')) // 错误原因为fail的promise对象代码实现如下。
Promise.reject2 = function(value) {
return new Promise((_, reject) => {
reject(value)
})
}Promise.race
接收一个可迭代的对象,并将最先执行完成的promise对象返回。
const promise = Promise.race([
new Promise((resolve, reject) => setTimeout(() => reject(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
])
promise.then(console.log) // 打印2代码实现。
Promise.race2 = function (promises) {
return new Promise((resolve, reject) => {
if (!promises[Symbol.iterator]) {
reject(new Error(`${typeof promises} ${promises} is not iterable`));
}
for (const promise of promises) {
Promise.resolve(promise).then(resolve, reject);
}
});
};Promise.all
若希望并行执行多个promise对象并等待所有promise都执行成功,可接收一个可迭代对象(通常为promise数组)。当迭代对象中每个值都被resolve时,会返回一个新的promise,并返回结果数组。当迭代对象中有任意一个值被reject时,直接返回新的promise,其状态为rejected。
const promise = Promise.all([
new Promise((resolve, reject) => setTimeout(() => reject(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
])
promise.then(console.log, co sole.error) // console.error打印1这里需要注意一点,结果数组的顺序与源promise的顺序一致,即使前面的promise耗费时间最长,其结果也会放置在结果数组第一个。
const promise = Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
])
promise.then(console.log) // 打印结果[1, 2, 3]针对图片加载示例,使用Promise.all来实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
function loadImage(imgUrl) {
return new Promise((resolve) => {
const img = document.createElement("img");
img.onload = function () {
resolve(this);
};
img.src = imgUrl;
});
}
const imgUrlList = [
"https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
"https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
"https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg",
];
const promiseList = imgUrlList.map((item) => loadImage(item));
const promise = Promise.all(promiseList).then((imglist) => {
imglist.forEach((item) => document.body.appendChild(item));
});
</script>
</html>代码实现。
Promise.all2 = function (promises) {
return new Promise((resolve, reject) => {
if (!promises[Symbol.iterator]) {
reject(new Error(`${typeof promises} ${promises} is not iterable`));
}
const len = promises.length;
const result = new Array(len);
let count = 0;
if (!len) {
resolve(result);
return;
}
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
(res) => {
count++;
result[i] = res; // 保证结果数组的放置顺序
if (count === len) {
resolve(result);
}
},
(err) => {
reject(err);
}
);
}
});
};Promise.allSettled
前文提到的Promise.all遇到任意一个promise reject,那么Promise.all会直接返回一个rejected的promise对象。而Promise.allSetled只需等待迭代对象内所有的值都完成了状态的转变,无论迭代对象里面的值是被resolve还是reject,就会返回一个状态为fulfilled的promise对象,并以包含对象数组的形式返回结果。
const promise = Promise.allSettled([
new Promise((resolve, reject) => setTimeout(() => reject(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]);
promise.then(console.log, console.error);
// 打印结果
// [
// { status: 'rejected', reason: 1 },
// { status: 'fulfilled', value: 2 },
// { status: 'fulfilled', value: 3 }
// ]代码实现。
Promise.allSettled2 = function (promises) {
const resolveHandler = (res) => ({ status: "fulfilled", value: res });
const rejectHandler = (err) => ({ status: "rejected", reason: err });
return Promise.all(
promises.map((item) => item.then(resolveHandler, rejectHandler))
);
};使用场景
大多数异步任务场景均可使用promise,例如网络请求、文件操作、数据库操作等。当然并非所有异步任务场景都适合使用promise,例如在事件驱动的编程模型中,使用时间监听器和触发器处理异步操作更为自然和直观。
在JavaScript中,async和await提供了基于promise更高级的异步编程方式,其使用方式看起来像同步操作一样,更加直观。在使用promise的同时,可配合async和await体验更好的异步编程。
一个小问题
前文在讲catch时提到,catch(errorHandler)其实就是then(null, errorHandler)的简写,那么下面两种写法会有区别吗?
// 写法1
promise.then(resolveHandler, rejectHandler)
// 写法2
promise.then(resolveHandler).catch(rejectHandler)答案是不一样,假如在resolveHandler里面抛出错误,写法1最终会获得一个rejected的promise,而写法二由于后续有catch方法,所以即使f1里面有抛出异常,也能得到处理。
以上就是JS解决回调地狱为什么需要Promise来优化异步编程的详细内容,更多关于JS Promise异步编程的资料请关注本站其它相关文章!