彻底搞懂 JS 异步:同步异步、事件循环与 Promise 全攻略

前言

前几天进行了一场面试,因为异步这块准备得并不充分,因此被面试官"按在地上摩擦"。于是,我决定痛定思痛,下定决心来彻底研究一下这个异步 、这个Promise 、这个Generator 、这个async/await

希望你阅读完这篇文章后能轻松回答出以下的几个问题:

  1. 说说你对同步和异步的理解?
  2. 说说你对事件循环的理解?能否描述一下整个过程
  3. 说说你对Promise的理解
  4. Promise到底解决了什么问题
  5. Promise的实例方法有哪些?
  6. Promise的静态方法有哪些?
  7. Promise.all和Promise.race的区别?

同步与异步的理解

JS是一个单线程的编程语言。意味着在一个特定的时间点,只有一个代码块在执行。渲染主线程负责处理网页的构建、布局、绘制和用户交互等任务,而异步编程可以使得我们可以在主线程执行同步代码的同时,处理耗时的异步操作。

根据上述描述,我们可以得知,异步操作不在主线程中运行,而是在另外的线程中运行。

同步

代码按照书写顺序依次执行,前一个任务完成后才能执行下一个任务。

同步的特点:

  • 按顺序执行
  • 会阻塞代码
  • 直接在主线程中执行

异步

代码不按顺序执行,而是通过回调函数、Promise、事件监听等机制,在某个操作完成触发后续逻辑。

异步的特点:

  • 异步任务不会阻塞主线程,运行在等待期间执行其他代码
  • 异步任务由浏览器或者 Node.js 等其他线程处理 ,完成后放入任务队列
  • 执行顺序不确定 ,依赖于事件循环来管理异步任务的

常见的异步操作:回调函数、Promise、Async/Await、事件监听

JS 是如何实现异步的呢?

通过事件循环 (Event Loop)和任务队列实现异步行为:

  1. 调用栈:执行同步代码,遇到异步操作时,将其交给浏览器的 Web API 处理
  2. 任务队列:当异步操作完成时,它的回调函数会被放入任务队列中
  3. 事件循环:当调用栈为空时,事件循环将队列中的回调函数推入调用栈执行

事件循环

  1. 执行 JS 代码,将不同函数的执行上下文压入执行栈中,保证代码的有序执行
  2. 在执行同步代码时,如果遇到异步事件,根据回调按类型放入不同的队列中等待:
  • 宏任务队列setTimeoutsetInterval、I/O 操作、DOM 事件、requestAnimationFrame(浏览器)、setImmediate(Node.js)。
  • 微任务队列Promise.thenMutationObserver(浏览器)、queueMicrotaskprocess.nextTick(Node.js,优先级高于普通微任务)。
  1. 当当前执行栈中的事件执行完毕后,JS 首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行
  2. 当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务
  3. 不断重复 3、4 步骤,直到所有队列为空,则执行完成

阅读完上述内容,想必你对同步、异步以及异步代码是如何执行的已经有个宏观的概念了吧,接下来我们将更详细的介绍一下回调函数promise 这两个常见的异步操作。

回调

回调函数:回调函数(Callback Function)是指将一个函数作为参数传递给另一个函数,并在某个特定条件满足时执行该函数。

js 复制代码
function fetchData(url,successCallBack,failureCallBack) {
  setTimeout(() => {
    if(url==='http://baidu.com'){
      const data = "Hello, World!";
      successCallBack(data); // 在异步操作完成后调用回调函数
    }else{
      failureCallBack('请求URL错误')
    }
  }, 1000);
}
function successData(data) {
  console.log("Received data:", data);
}
function failureData(data){
  console.log('请求错误原因:',data)
}
fetchData('http://baidu.com',successData,failureData); // 传递回调函数

听到上述这个概念的时候,我想到的却是闭包的产生。让我们来分析一下是否产生了闭包:

闭包:是指一个函数能够「记住」并访问其词法作用域中的变量,即使这个函数是在其定义的作用域之外被调用。

  1. fetchData 执行,将 processData 作为参数传入。processData 就是在 fetchData 词法作用域中。
  2. 遵循同步优先,异步延迟的规则。fetchData 执行完毕后,退出执行栈。然后异步代码进入执行栈,由于 setTimeout 采用的是箭头函数,箭头函数与外部的 fetchData 作用域一致。
  3. setTimeout 在 1 秒后触发,箭头函数仍然需要 访问callback,即时 fetchData 函数已经执行完毕并退出调用栈。
  4. 因此上述代码确实产生了闭包

回调地狱

阅读到这里想必你对回调函数和闭包已经有了一定的了解,接下来我们来讲讲使用回调函数所面临的问题:回调地狱

假设我们正在开发一个博客应用,需要完成以下任务:

  1. 根据用户 ID 获取用户信息。
  2. 根据用户信息获取该用户的所有博客文章。
  3. 根据每篇博客文章的 ID 获取该文章的评论。
js 复制代码
function getUser(userId, callback) {
    setTimeout(() => {
        const user = { id: userId, name: "John Doe" };
        console.log("User fetched:", user);
        callback(user);
    }, 1000);
}

function getPosts(userId, callback) {
    setTimeout(() => {
        const posts = [{ id: 1, title: "Post 1" }, { id: 2, title: "Post 2" }];
        console.log("Posts fetched for user:", userId, posts);
        callback(posts);
    }, 1000);
}

function getComments(postId, callback) {
    setTimeout(() => {
        const comments = [{ id: 1, text: "Great post!" }, { id: 2, text: "Nice work!" }];
        console.log("Comments fetched for post:", postId, comments);
        callback(comments);
    }, 1000);
}

// 使用回调函数处理异步操作
getUser(1, function(user) {
    getPosts(user.id, function(posts) {
        posts.forEach(post => {
            getComments(post.id, function(comments) {
                console.log("Comments for post", post.id, ":", comments);
            });
        });
    });
});

上述代码很明显得给我们展现了回调地狱所存在的一些问题:

  1. 嵌套过深:代码逐层嵌套,每增加一个异步操作,嵌套层级就会增加一层。
  2. 可读性差:嵌套的回调函数使得代码难以阅读和理解,尤其是当逻辑变得更加复杂时。
  3. 错误处理困难:如果需要在每个异步操作中处理错误,错误处理代码会分散在各个回调中,难以统一管理。

Promise

通过回调函数的方式确实是可以解决请求函数得到结果之后,获取到对应的回调,但是它存在两个主要的问题:

  1. 需要我们自己来设计回调函数、回调函数的名称、回调函数的使用等
  2. 对于不同的人、不同的框架设计出来的方案是不同的,因此需要花费一定的时间去了解如何使用这个函数。

Promise 更像是一种规定、一种规范,它标准化了回调机制,同时由于是 ES6 引入,得到了广泛的支持:

  • 使用 .then() 处理成功的结果。
  • 使用 .catch() 处理失败的结果。
  • 使用 .finally() 处理无论成功或失败都需要执行的逻辑。
js 复制代码
const promise = new Promise((resolve, reject) => {
  // 调用resolve, 那么then传入的回调会被执行
  resolve("哈哈哈")
  // 调用reject, 那么catch传入的回调会被执行
  reject("错误信息")
})

promise.then(res => {
  console.log(res)
}).catch(err => {
  console.log(err)
})

在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor。上面Promise使用过程,我们可以将它划分成三个状态:

  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝;

    • 当执行executor中的代码时,处于该状态;
  • 已兑现(fulfilled): 意味着操作成功完成;

    • 执行了resolve时,处于该状态;
  • 已拒绝(rejected): 意味着操作失败;

    • 执行了reject时,处于该状态;

注意:状态不可逆 :一旦状态变为 FulfilledRejected,就不能再改变。

Promise.resolve

作用: 快速创建一个状态为fulfiled的 Promise 对象,支持将普通值、Promise对象或者 thenabel 对转为 Promise

转化规则:

  1. 若为普通值:直接作为成功结果返回
  2. 若为 Promise 实例:直接返回该实例,同时新 Promise 会决定原 Promise 的状态
  3. 若为 thenable 对象:就是对象中定义了 then 方法的对象,会先执行 then 方法,并且根据 then 方法的结果来决定 Promise 的状态

Promise.reject

作用:快速创建一个状态为 rejected(已拒绝) 的 Promise 对象,参数作为拒绝原因

转化规则:

  1. 任意类型的值(错误对象、字符串等),均会被包裹为拒绝原因。
  2. resolve 不同:即使传入 Promise 实例,也会将其作为普通值处理(不会展开)

阅读完上述内容后,相比你对 Promise.resolve 与 Promise.reject 的使用有了一定的了解,那么查看以下代码,回答下列的值

js 复制代码
Promise.resolve(Promise.reject(1))
  .then((value) => console.log('Resolved:', value))
  .catch((error) => console.error('Rejected:', error));

Promise.reject(Promise.resolve(1))
  .then((value) => console.log('Resolved:', value))
  .catch((error) => console.error('Rejected:', error));

答案写在评论区了🤓😉😉😉😉

为什么说 Promise 可以解决回调地狱

针对回调地狱那个代码,我们现在采用 promise 来进行编写:

js 复制代码
// 将每个异步函数改为返回 Promise
function getUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const user = { id: userId, name: "John Doe" };
      console.log("User fetched:", user);
      resolve(user);
    }, 1000);
  });
}

function getPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const posts = [{ id: 1, title: "Post 1" }, { id: 2, title: "Post 2" }];
      console.log("Posts fetched for user:", userId, posts);
      resolve(posts);
    }, 1000);
  });
}

function getComments(postId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const comments = [{ id: 1, text: "Great post!" }, { id: 2, text: "Nice work!" }];
      console.log("Comments fetched for post:", postId, comments);
      resolve(comments);
    }, 1000);
  });
}

// 使用 Promise 链式调用处理异步操作
getUser(1)
  .then(user => getPosts(user.id))
  .then(posts => {
    // 对每个 post 获取评论,返回一个 Promise 数组
    const commentPromises = posts.map(post => getComments(post.id));
    // 使用 Promise.all 等待所有评论获取完成
    return Promise.all(commentPromises);
  })
  .then(commentsArray => {
    // commentsArray 是一个数组,每个元素是对应 post 的评论
    commentsArray.forEach((comments, index) => {
      console.log("Comments for post", index + 1, ":", comments);
    });
  })
  .catch(error => {
    console.error("Error:", error);
  });

相比于回调地狱,有着以下明显的优势:

  1. 链式调用:通过链式调用(.then())将异步操作串联起来,代码结构扁平化,逻辑更加清晰。
  2. 错误处理:Promise 提供了统一的错误处理机制,可以通过.catch()方法捕获链式调用中的任何错误。
  3. 并行执行:通过 Promise.all() 可以轻松实现多个异步操作的并行执行,并等待所有操作完成。
  4. 状态管理可靠:Promise 的状态一旦从 Pending 变为 FulfilledRejected,就不会再改变,行为更加可预测。

Promise 对象方法

Promise 对象方法,就是放在 Promise 的原型上的 Promise.prototype

1. then

  1. 接受两个参数:
  • onFulfilled:当 Promise 被成功解决(fulfilled)时调用的回调函数,接收解决值为参数

    • 如果省略,则成功值会传递给链中的下一个 .then
  • onRejected:当 Promise 被拒绝(rejected)时调用的回调函数,拒绝原因作为参数

    • 如果省略,则拒绝原因会传递给链中的下一个 .catch.thenonRejected
js 复制代码
promise.then(onFulfilled, onRejected);
//等同于:
promise.then(onFulfilled).catch(onRejected)
  1. 返回值: .then()总是会返回一个新的 Promise,其状态由回调函数的执行结果来决定:
回调返回值类型 新 Promise 状态 结果
普通值(非 Promise) fulfilled 该返回值
Promise 对象 与该 Promise 一致 其解决值/拒绝原因
thenable 对象 会先执行 then 方法,并且根据 then 方法的结果来决定 Promise 的状态 根据执行的结果来决定
抛出异常 rejected 抛出的错误对象
无返回值(即 undefined fulfilled undefined
  1. 多次调用 then
  • 多次调用 .then() :在同一个 Promise 实例上多次调用 .then(),会依次注册多个回调函数。
  • 触发时机 :当 Promise 状态变为 fulfilledrejected 时,所有对应的回调函数(onFulfilledonRejected)会被依次执行。
  • 独立执行 :每个 .then() 的返回值都是一个 新的 Promise,但这些新 Promise 彼此独立,互不影响。
js 复制代码
const promise = Promise.resolve('Hello');

promise.then((value) => {
  console.log('First then:', value); // 输出: First then: Hello
});

promise.then((value) => {
  console.log('Second then:', value); // 输出: Second then: Hello
});

2. catch

  1. onRejected:一个回调函数,当 Promise 被拒绝时会被调用。它接收一个参数,即拒绝原因(通常是错误对象)。
js 复制代码
promise.catch(onRejected);
  1. 返回值 :返回一个新的 Promise,其状态由 onRejected 回调函数的执行结果决定。
回调返回值类型 新 Promise 状态 结果
普通值(非 Promise) fulfilled 该返回值
Promise 对象 与该 Promise 一致 其解决值/拒绝原因
thenable 对象 会先执行 then 方法,并且根据 then 方法的结果来决定 Promise 的状态 根据执行的结果来决定
抛出异常 rejected 抛出的错误对象
无返回值(即 undefined fulfilled undefined
  1. 多次调用
  • 每次调用我们都可以传入对应的reject回调
  • 当Promise的状态变成reject的时候,这些回调函数都会被执行
js 复制代码
const promise = Promise.reject(new Error('Something went wrong'));

// 第一次调用 catch
promise.catch((error) => {
  console.error('First catch:', error.message); // 输出: First catch: Something went wrong
});

// 第二次调用 catch
promise.catch((error) => {
  console.error('Second catch:', error.message); // 输出: Second catch: Something went wrong
});

3. finally

finally方法是不接收参数的,因为无论前面是fulfilled状态,还是reject状态,它都会执行。

js 复制代码
const promise = new Promise((resolve, reject) => {
  // resolve("fulfilled")
  reject("reject")
})

promise.then(res => {
  console.log("res:", res)
}).catch(err => {
  console.log("err:", err)
}).finally(() => {
  console.log("finally action")
})

Promise 静态方法

Promise 是 JavaScript 中用于处理异步操作的对象。除了实例方法(如 thencatchfinally),Promise还提供了一些静态方法,用于处理多个Promise实例或创建特定的Promise对象。以下是 Promise 的静态方法:

  1. Promise.resolve:参考之前的内容
  2. Promise.reject:参考之前的内容

3. Promise.all(iterable)

  • 作用 : 返回一个 Promise,当所有传入的 Promise 都成功解析时,它才会解析;如果其中任何一个 Promise 被拒绝,它就会立即被拒绝。
  • 参数 : iterable 是一个可迭代对象(如数组),包含多个 Promise 对象。
  • 返回值 : 一个 Promise,解析值为所有 Promise 解析值的数组。
js 复制代码
Promise.all([Promise.resolve(1), Promise.resolve(2)]).then(values => console.log(values)); // 输出: [1, 2]

4. Promise.allSettled(iterable)

  • 作用 : 返回一个 Promise,当所有传入的 Promise 都完成(无论成功或失败)时,它才会解析。
  • 参数 : iterable 是一个可迭代对象,包含多个 Promise 对象。
  • 返回值 : 一个 Promise,解析值为一个对象数组,每个对象表示对应 Promise 的状态和值/原因。
js 复制代码
Promise.allSettled([Promise.resolve(1), Promise.reject('error')])
  .then(results => console.log(results));
// 输出: [{ status: 'fulfilled', value: 1 }, { status: 'rejected', reason: 'error' }]

5. Promise.any(iterable)

  • 作用 : 返回一个 Promise,当传入的 Promise 中任何一个成功解析时,它就会解析;如果所有 Promise 都被拒绝,它就会被拒绝。
  • 参数 : iterable 是一个可迭代对象,包含多个 Promise 对象。
  • 返回值 : 一个 Promise,解析值为第一个成功解析的 Promise 的值。
js 复制代码
Promise.any([Promise.reject('error'), Promise.resolve(1)])
  .then(value => console.log(value)); // 输出: 1

6. Promise.race(iterable)

  • 作用 : 返回一个 Promise,当传入的 Promise 中任何一个完成(无论成功或失败)时,它就会立即完成。
  • 参数 : iterable 是一个可迭代对象,包含多个 Promise 对象。
  • 返回值 : 一个 Promise,解析值或拒绝原因与第一个完成的 Promise 相同。
js 复制代码
Promise.race([Promise.resolve(1), Promise.reject('error')])
  .then(value => console.log(value)); // 输出: 1

总结

  • Promise.resolvePromise.reject 用于创建已解析或已拒绝的 Promise
  • Promise.all 用于等待所有 Promise 成功解析。
  • Promise.allSettled 用于等待所有 Promise 完成(无论成功或失败)。
  • Promise.any 用于等待任何一个 Promise 成功解析。
  • Promise.race 用于等待任何一个 Promise 完成(无论成功或失败)。

这些静态方法在处理多个异步操作时非常有用,可以根据不同的需求选择合适的工具。

写在最后

如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!

接下来我将对yieldGenerator等内容进行详细的介绍,记得关注我哦。

参考文档

  1. JavaScript高级系列(十七) - Promise用法的详细解析
  2. 每天3分钟,重学ES6-ES12(十)Promise参数实例方法介绍
  3. 每天3分钟,重学ES6-ES12(十一)Promise的类方法
相关推荐
Fantasywt3 小时前
THREEJS 片元着色器实现更自然的呼吸灯效果
前端·javascript·着色器
IT、木易4 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
张拭心6 小时前
2024 总结,我的停滞与觉醒
android·前端
念九_ysl6 小时前
深入解析Vue3单文件组件:原理、场景与实战
前端·javascript·vue.js
Jenna的海糖6 小时前
vue3如何配置环境和打包
前端·javascript·vue.js
星之卡比*7 小时前
前端知识点---库和包的概念
前端·harmonyos·鸿蒙
灵感__idea7 小时前
Vuejs技术内幕:数据响应式之3.x版
前端·vue.js·源码阅读
烛阴7 小时前
JavaScript 构造器进阶:掌握 “new” 的底层原理,写出更优雅的代码!
前端·javascript
Alan-Xia7 小时前
使用jest测试用例之入门篇
前端·javascript·学习·测试用例
浪遏7 小时前
面试官😏 :文本太长,超出部分用省略号 ,怎么搞?我:🤡
前端·面试