引言
今天,我浅浅研究了 JavaScript 中的
Promise
机制,感到非常兴奋,想着写一篇文章来梳理这些新学到的知识,并与大家分享我的心得和见解,希望通过这篇文章,能够帮助大家更好地理解Promise
的魅力。🥳🥳
初探 Promise 输出顺序
js
const p = new Promise(() => { // executor 执行器
console.log('333'); // 同步任务
setTimeout(() => { // 异步任务 event loop(宏任务)
console.log('222')
}, 1000)
})
console.log('111');
我们发现打印结果如上图所示,输出的结果是 333
紧接着是 111
,最后才是 222
,为什么会这样输出呢?这背后其实涉及到 JavaScript 的执行模型------基于事件循环(event loop)的工作原理。
输出顺序解析
JavaScript 是一种单线程语言,所有任务都必须排队等待前面的任务完成才能开始。当创建一个新的 Promise
时,传入的执行器函数(executor)会立即同步执行,因此 console.log('333')
作为同步任务的一部分被执行。紧接着,setTimeout
被调用并安排了一个异步任务,在事件循环的下一个宏任务周期中执行。而 console.log('111')
则紧跟在 Promise
创建之后的同步任务中,所以它会在所有同步代码块结束后立即执行,但在这之前,由于 Promise
的构造函数是同步执行的,所以 333
会先被打印出来,然后是 111
,最后才是由宏任务触发的 222
。
如果我们想先输出 222
,再输出 111
,我们需要重新设计我们的异步逻辑来确保顺序。这就要请到我们今天的主角------Promise
来帮助我们控制异步操作的流程🧐🧐。
关于宏任务与微任务的补充说明
为了更全面地理解上述代码的行为,我们需要了解 JavaScript 的事件循环系统如何处理宏任务和微任务。在这段代码中:
- 宏任务(Macrotask) :
setTimeout
是一个宏任务,它会在当前任务完成后加入到宏任务队列中等待执行。 - 微任务(Microtask) :由
Promise
触发的.then()
和.catch()
回调则是微任务,它们会在每次宏任务执行完毕后立即处理,直到所有可用的微任务都已处理完毕。
这意味着,一旦当前的同步任务完成,JavaScript 引擎会首先检查并执行所有的微任务,然后才会回到事件循环,继续处理下一个宏任务。这解释了为什么 222
的输出会被延迟到最末尾------因为它是由宏任务 setTimeout
触发的,必须等到当前同步任务以及所有微任务完成后才会执行。
深入介绍 Promise
流程图:
作用
Promise
是 ES6 引入的一种对象,它代表了一个异步操作的最终完成(或失败)及其结果值。通过 Promise
,我们可以定义一系列的异步操作,并指定在这些操作成功完成后要做什么,或者如果失败了又该如何响应。这种方式不仅简化了代码结构,也提高了可读性和维护性,使我们能够更加直观地控制异步操作的执行流程。
结构
Promise
接受一个带有两个参数的函数:resolve
和 reject
。这个函数被称为执行器(executor),它会在 Promise
实例创建时立即执行。执行器内部通常包含一些需要异步执行的操作。根据异步操作的结果,执行器可以调用 resolve
或 reject
函数来改变 Promise
的状态。
- Resolve :当异步操作成功完成时,调用
resolve
函数可以使Promise
进入 fulfilled 状态,这时可以通过.then()
方法获取结果。 - Reject :如果异步操作遇到错误,调用
reject
函数会使Promise
进入 rejected 状态,此时可以用.catch()
捕获错误。
此外,Promise
有三种可能的状态:
- Pending(待定) :这是
Promise
的初始状态,既不是成功也不是失败。 - Fulfilled(已实现) :表示异步操作已经成功完成,并返回了结果。
- Rejected(已拒绝) :表示异步操作失败,并返回了错误原因。
一旦 Promise
的状态从 pending 变为 fulfilled 或 rejected,它的状态就不可逆转,也就是说,Promise
一旦解决(fulfilled 或 rejected),就不会再次改变。
三种实例方法
1. .then()
方法
.then()
方法用于注册当 Promise
成功解决(fulfilled)时要执行的回调函数。它可以接收两个参数:
- 第一个参数 :一个函数,在
Promise
被resolve
后调用,通常用来处理成功的结果。 - 第二个参数 (可选):另一个函数,在
Promise
被reject
后调用,通常用来处理错误。不过,使用.catch()
是更常见的做法。
2. .catch()
方法
.catch()
方法专门用于捕获并处理 Promise
链中任何地方抛出的错误或被 reject
的情况。它只接受一个参数------一个错误处理函数。这个方法特别有用,因为它可以集中处理所有可能发生的错误,而不需要在每个 .then()
中都编写错误处理逻辑。
3..finally()
方法
除了 .then()
和 .catch()
外,Promise
还提供了一个 .finally()
方法。无论 Promise
最终是 fulfilled 还是 rejected,.finally()
中的回调都会被执行。这使得 .finally()
成为了清理资源、关闭连接或执行任何必须在异步操作结束后运行代码的理想选择。它不会接收任何参数,因为它不关心 Promise
的最终状态是什么。
通过代码深入了解promise的运行流程
让我们看看下面这段代码是如何工作的:
js
// 实例化,传递函数,里面装耗时性任务
const p = new Promise((resolve, reject) => { // executor 执行器
setTimeout(() => { // 异步任务 event loop(宏任务)
console.log('定时器执行了')
// reject() // 失败
// resolve('BMW325')
}, 1000)
})
console.log(p.__proto__, p);
// <pending>: 等待状态
p
.then((data) => {
// 等到executor 异步任务执行完后,再执行then 里面的函数
console.log(p);
// <fulfilled>: 成功状态
console.log(data);
})
.catch(() => {
console.log(p);
console.log('error');
})
在这个例子中,Promise
构造函数内的代码是同步执行的,因此 console.log('定时器执行了')
会被推迟到下一个宏任务周期执行。而 console.log(p.__proto__, p)
会立即执行,显示 p
的原型和当前实例,此时 p
应该处于 pending 状态。
修改后的代码示例
如果我们想确保 Promise
正常工作,并且希望看到 .then()
和 .catch()
的效果,可以对代码进行如下修改:
js
// 实例化,传递函数,里面装耗时性任务
const p = new Promise((resolve, reject) => { // executor 执行器
setTimeout(() => { // 异步任务 event loop(宏任务)
console.log('定时器执行了')
// reject() // 失败
resolve('BMW325')
}, 1000)
})
......
在这个版本中,我们取消了对 resolve('BMW325')
的注释,使得 Promise
可以进入 fulfilled
状态,并触发 .then()
方法中的回调函数。如果需要测试错误处理逻辑,可以取消对 reject()
的注释,并观察 .catch()
的行为。
发现.catch()的行为如下图所示:
当 reject()
被调用时,Promise
将进入 rejected 状态,这时 .catch()
中的回调函数会被触发,从而允许我们处理错误信息。这样的设计使我们可以在不干扰正常执行路径的情况下优雅地处理异常情况。
在了解了promise的作用与工作流程后,试试实现输出
js
const p = new Promise((resolve) => { // executor 执行器
console.log('333'); // 同步任务
setTimeout(() => { // 异步任务 event loop(宏任务)
console.log('222')
resolve('BMW325')
}, 1000)
})
.then(() => {
console.log('111');
})
在这里使用 Promise
控制异步操作的执行顺序,而resolve
函数在异步任务完成后被调用,使得 111
的输出可以在 222
之后发生。
延伸:用promise 手写Ajax
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手写 Ajax</title>
</head>
<body>
<ul id="members"></ul>
<script>
// https://api.github.com/orgs/lemoncode/members URL -> http -> http(200 + 4) -> 异步耗时任务 -> 执行流程(DOM) -> promise
const getJSON = function (url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest() // 浏览器嗅探 IE 早期不支持
? new XMLHttpRequest()
: new ActiveXObject("Microsoft.XMLHTTP"); // 微软推出的核心对象
// 第三个参数 是否异步
xhr.open("GET", url, true)
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
// 304 Not Modified
// 第一次 查找 200 后端开销
// 之后来, 只要后端数据没有发生改变, 没有必要再去查
// 304, 不传内容
// 告诉浏览器, 直接使用本地数据
if (xhr.status === 200 || xhr.status === 304) {
resolve(JSON.parse(xhr.responseText))
}
else {
reject(new Error(xhr.responseText))
}
}
xhr.send();
})
}
getJSON('https://api.github.com/orgs/lemoncode/members')
.then(data => {
console.log(data)
})
</script>
</body>
</html>
介绍一下 XMLHttpRequest
XMLHttpRequest
是一个内置的 Web API,用于在浏览器中发起 HTTP 请求并与服务器交互。它支持发送同步和异步请求,默认情况下是异步的。XMLHttpRequest
对象提供了多种属性和方法来配置请求和处理响应。在现代开发中,虽然 Fetch API 已经成为新的标准,但是了解 XMLHttpRequest
仍然是有价值的,因为它在旧版浏览器中有更好的兼容性。
代码解释
这段代码演示了如何使用 Promise
封装一个 AJAX 请求,具体步骤如下:
- 创建
getJSON
函数 :此函数接收一个 URL 参数,并返回一个新的Promise
对象。 - 初始化 XHR 对象 :使用
XMLHttpRequest
创建一个 HTTP 请求对象。 - 配置请求:设置请求的方法(GET)、URL 和是否异步(true)。
- 监听状态变化 :通过
onreadystatechange
监听请求的状态变化,当状态码为 4 时(即请求完成),根据响应的状态码决定是调用resolve
还是reject
。 - 发送请求 :调用
send()
发起请求。 - 处理响应 :在
.then()
中处理成功的响应数据,在.catch()
中捕获并处理可能发生的错误。
这样做不仅使代码更易读,还允许我们利用 Promise
的链式调用来简化复杂的异步流程控制。同时,我们还添加了错误处理逻辑,确保即使请求失败也能得到适当的反馈。
工具方法
Promise
还提供了一些工具方法,可以帮助我们同时处理多个 Promise
,例如 Promise.all
、Promise.race
、Promise.allSettled
和 Promise.any
。这些工具方法在需要并发执行多个异步操作时非常有用,可以根据不同的需求选择合适的方法来优化代码性能和用户体验。感兴趣的朋友可以前往Promise - JavaScript | MDN了解更多细节。
总结
希望这篇文章能够为你带来启发,并帮助你在日常工作中更加熟练地运用 Promise
,如果你觉得这篇文章对你有所帮助,欢迎点赞、评论和支持!一起交流学习,共同进步!😊✨