深入浅出:掌握 JavaScript 异步编程的秘密武器——Promise!!🥳🥳

引言

今天,我浅浅研究了 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 接受一个带有两个参数的函数:resolvereject。这个函数被称为执行器(executor),它会在 Promise 实例创建时立即执行。执行器内部通常包含一些需要异步执行的操作。根据异步操作的结果,执行器可以调用 resolvereject 函数来改变 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)时要执行的回调函数。它可以接收两个参数:

  • 第一个参数 :一个函数,在 Promiseresolve 后调用,通常用来处理成功的结果。
  • 第二个参数 (可选):另一个函数,在 Promisereject 后调用,通常用来处理错误。不过,使用 .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 请求,具体步骤如下:

  1. 创建 getJSON 函数 :此函数接收一个 URL 参数,并返回一个新的 Promise 对象。
  2. 初始化 XHR 对象 :使用 XMLHttpRequest 创建一个 HTTP 请求对象。
  3. 配置请求:设置请求的方法(GET)、URL 和是否异步(true)。
  4. 监听状态变化 :通过 onreadystatechange 监听请求的状态变化,当状态码为 4 时(即请求完成),根据响应的状态码决定是调用 resolve 还是 reject
  5. 发送请求 :调用 send() 发起请求。
  6. 处理响应 :在 .then() 中处理成功的响应数据,在 .catch() 中捕获并处理可能发生的错误。

这样做不仅使代码更易读,还允许我们利用 Promise 的链式调用来简化复杂的异步流程控制。同时,我们还添加了错误处理逻辑,确保即使请求失败也能得到适当的反馈。

工具方法

Promise 还提供了一些工具方法,可以帮助我们同时处理多个 Promise,例如 Promise.allPromise.racePromise.allSettledPromise.any。这些工具方法在需要并发执行多个异步操作时非常有用,可以根据不同的需求选择合适的方法来优化代码性能和用户体验。感兴趣的朋友可以前往Promise - JavaScript | MDN了解更多细节。

总结

希望这篇文章能够为你带来启发,并帮助你在日常工作中更加熟练地运用 Promise,如果你觉得这篇文章对你有所帮助,欢迎点赞、评论和支持!一起交流学习,共同进步!😊✨

相关推荐
Pee Wee34 分钟前
责任链模式
java·前端·责任链模式
我家媳妇儿萌哒哒1 小时前
vue Element Ui Upload 上传 点击一个按钮,选择多个文件后直接上传,使用防抖解决多次上传的问题。
前端·javascript·vue.js
前端青山1 小时前
JavaScript闭包的深度剖析与实际应用
开发语言·前端·javascript·前端框架·ecmascript
JINGWHALE11 小时前
设计模式 结构型 组合模式(Composite Pattern)与 常见技术框架应用 解析
前端·人工智能·后端·设计模式·性能优化·系统架构·组合模式
青颜的天空1 小时前
CSS 中 content换行符实现打点 loading 正在加载中的效果
前端·css
DX_水位流量监测2 小时前
水库水位监测系统的自动化功能:减少人工干预,可实现实时监控
运维·前端·人工智能·自动化·制造·信息与通信·零售
m0_548503032 小时前
打包部署若依(RuoYi)SpringBoot后端和Vue前端图文教程
前端·vue.js·spring boot
阿芯爱编程2 小时前
清除数字栈
java·服务器·前端
GISer_Jing3 小时前
React函数组件中与生命周期相关Hooks详解
前端·javascript·react.js
要加油哦~4 小时前
尚硅谷· vue3+ts 知识点学习整理 |14h的课程(持续更ing)
javascript·vue.js·学习