前言
前几天进行了一场面试,因为异步这块准备得并不充分,因此被面试官"按在地上摩擦"。于是,我决定痛定思痛,下定决心来彻底研究一下这个异步 、这个Promise 、这个Generator 、这个async/await。
希望你阅读完这篇文章后能轻松回答出以下的几个问题:
- 说说你对同步和异步的理解?
- 说说你对事件循环的理解?能否描述一下整个过程
- 说说你对Promise的理解
- Promise到底解决了什么问题
- Promise的实例方法有哪些?
- Promise的静态方法有哪些?
- Promise.all和Promise.race的区别?
同步与异步的理解
JS是一个单线程的编程语言。意味着在一个特定的时间点,只有一个代码块在执行。渲染主线程负责处理网页的构建、布局、绘制和用户交互等任务,而异步编程可以使得我们可以在主线程执行同步代码的同时,处理耗时的异步操作。
根据上述描述,我们可以得知,异步操作不在主线程中运行,而是在另外的线程中运行。
同步
代码按照书写顺序依次执行,前一个任务完成后才能执行下一个任务。
同步的特点:
- 按顺序执行
- 会阻塞代码
- 直接在主线程中执行
异步
代码不按顺序执行,而是通过回调函数、Promise、事件监听等机制,在某个操作完成触发后续逻辑。
异步的特点:
- 异步任务不会阻塞主线程,运行在等待期间执行其他代码
- 异步任务由浏览器或者 Node.js 等其他线程处理 ,完成后放入任务队列
- 执行顺序不确定 ,依赖于事件循环来管理异步任务的
常见的异步操作:回调函数、Promise、Async/Await、事件监听
JS 是如何实现异步的呢?
通过事件循环 (Event Loop)和任务队列实现异步行为:
- 调用栈:执行同步代码,遇到异步操作时,将其交给浏览器的 Web API 处理
- 任务队列:当异步操作完成时,它的回调函数会被放入任务队列中
- 事件循环:当调用栈为空时,事件循环将队列中的回调函数推入调用栈执行
事件循环
- 执行 JS 代码,将不同函数的执行上下文压入执行栈中,保证代码的有序执行
- 在执行同步代码时,如果遇到异步事件,根据回调按类型放入不同的队列中等待:
- 宏任务队列 :
setTimeout
、setInterval
、I/O 操作、DOM 事件、requestAnimationFrame
(浏览器)、setImmediate
(Node.js)。 - 微任务队列 :
Promise.then
、MutationObserver
(浏览器)、queueMicrotask
、process.nextTick
(Node.js,优先级高于普通微任务)。
- 当当前执行栈中的事件执行完毕后,JS 首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行
- 当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务
- 不断重复 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); // 传递回调函数
听到上述这个概念的时候,我想到的却是闭包的产生。让我们来分析一下是否产生了闭包:
闭包:是指一个函数能够「记住」并访问其词法作用域中的变量,即使这个函数是在其定义的作用域之外被调用。
- fetchData 执行,将 processData 作为参数传入。processData 就是在 fetchData 词法作用域中。
- 遵循同步优先,异步延迟的规则。fetchData 执行完毕后,退出执行栈。然后异步代码进入执行栈,由于 setTimeout 采用的是箭头函数,箭头函数与外部的 fetchData 作用域一致。
- setTimeout 在 1 秒后触发,箭头函数仍然需要 访问
callback
,即时 fetchData 函数已经执行完毕并退出调用栈。 - 因此上述代码确实产生了闭包
回调地狱
阅读到这里想必你对回调函数和闭包已经有了一定的了解,接下来我们来讲讲使用回调函数所面临的问题:回调地狱。
假设我们正在开发一个博客应用,需要完成以下任务:
- 根据用户 ID 获取用户信息。
- 根据用户信息获取该用户的所有博客文章。
- 根据每篇博客文章的 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);
});
});
});
});
上述代码很明显得给我们展现了回调地狱所存在的一些问题:
- 嵌套过深:代码逐层嵌套,每增加一个异步操作,嵌套层级就会增加一层。
- 可读性差:嵌套的回调函数使得代码难以阅读和理解,尤其是当逻辑变得更加复杂时。
- 错误处理困难:如果需要在每个异步操作中处理错误,错误处理代码会分散在各个回调中,难以统一管理。
Promise
通过回调函数的方式确实是可以解决请求函数得到结果之后,获取到对应的回调,但是它存在两个主要的问题:
- 需要我们自己来设计回调函数、回调函数的名称、回调函数的使用等
- 对于不同的人、不同的框架设计出来的方案是不同的,因此需要花费一定的时间去了解如何使用这个函数。
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时,处于该状态;
注意:状态不可逆 :一旦状态变为 Fulfilled
或 Rejected
,就不能再改变。
Promise.resolve
作用: 快速创建一个状态为fulfiled
的 Promise 对象,支持将普通值、Promise
对象或者 thenabel
对转为 Promise
转化规则:
- 若为普通值:直接作为成功结果返回
- 若为 Promise 实例:直接返回该实例,同时新 Promise 会决定原 Promise 的状态
- 若为 thenable 对象:就是对象中定义了 then 方法的对象,会先执行 then 方法,并且根据 then 方法的结果来决定 Promise 的状态
Promise.reject
作用:快速创建一个状态为 rejected
(已拒绝) 的 Promise 对象,参数作为拒绝原因
转化规则:
- 任意类型的值(错误对象、字符串等),均会被包裹为拒绝原因。
- 与
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);
});
相比于回调地狱,有着以下明显的优势:
- 链式调用:通过链式调用(
.then()
)将异步操作串联起来,代码结构扁平化,逻辑更加清晰。 - 错误处理:Promise 提供了统一的错误处理机制,可以通过
.catch()
方法捕获链式调用中的任何错误。 - 并行执行:通过
Promise.all()
可以轻松实现多个异步操作的并行执行,并等待所有操作完成。 - 状态管理可靠:
Promise
的状态一旦从Pending
变为Fulfilled
或Rejected
,就不会再改变,行为更加可预测。
Promise 对象方法
Promise 对象方法,就是放在 Promise 的原型上的 Promise.prototype
1. then
- 接受两个参数:
-
onFulfilled
:当 Promise 被成功解决(fulfilled)时调用的回调函数,接收解决值为参数- 如果省略,则成功值会传递给链中的下一个
.then
- 如果省略,则成功值会传递给链中的下一个
-
onRejected
:当 Promise 被拒绝(rejected)时调用的回调函数,拒绝原因作为参数- 如果省略,则拒绝原因会传递给链中的下一个
.catch
或.then
的onRejected
。
- 如果省略,则拒绝原因会传递给链中的下一个
js
promise.then(onFulfilled, onRejected);
//等同于:
promise.then(onFulfilled).catch(onRejected)
- 返回值:
.then()
总是会返回一个新的 Promise,其状态由回调函数的执行结果来决定:
回调返回值类型 | 新 Promise 状态 | 结果 |
---|---|---|
普通值(非 Promise) | fulfilled | 该返回值 |
Promise 对象 | 与该 Promise 一致 | 其解决值/拒绝原因 |
thenable 对象 | 会先执行 then 方法,并且根据 then 方法的结果来决定 Promise 的状态 | 根据执行的结果来决定 |
抛出异常 | rejected | 抛出的错误对象 |
无返回值(即 undefined ) |
fulfilled | undefined |
- 多次调用
then
:
- 多次调用
.then()
:在同一个 Promise 实例上多次调用.then()
,会依次注册多个回调函数。 - 触发时机 :当 Promise 状态变为 fulfilled 或 rejected 时,所有对应的回调函数(
onFulfilled
或onRejected
)会被依次执行。 - 独立执行 :每个
.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
onRejected
:一个回调函数,当 Promise 被拒绝时会被调用。它接收一个参数,即拒绝原因(通常是错误对象)。
js
promise.catch(onRejected);
- 返回值 :返回一个新的 Promise,其状态由
onRejected
回调函数的执行结果决定。
回调返回值类型 | 新 Promise 状态 | 结果 |
---|---|---|
普通值(非 Promise) | fulfilled | 该返回值 |
Promise 对象 | 与该 Promise 一致 | 其解决值/拒绝原因 |
thenable 对象 | 会先执行 then 方法,并且根据 then 方法的结果来决定 Promise 的状态 | 根据执行的结果来决定 |
抛出异常 | rejected | 抛出的错误对象 |
无返回值(即 undefined ) |
fulfilled | undefined |
- 多次调用:
- 每次调用我们都可以传入对应的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 中用于处理异步操作的对象。除了实例方法(如 then
、catch
、finally
),Promise还提供了一些静态方法,用于处理多个Promise
实例或创建特定的Promise
对象。以下是 Promise
的静态方法:
- Promise.resolve:参考之前的内容
- 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.resolve
和Promise.reject
用于创建已解析或已拒绝的Promise
。Promise.all
用于等待所有Promise
成功解析。Promise.allSettled
用于等待所有Promise
完成(无论成功或失败)。Promise.any
用于等待任何一个Promise
成功解析。Promise.race
用于等待任何一个Promise
完成(无论成功或失败)。
这些静态方法在处理多个异步操作时非常有用,可以根据不同的需求选择合适的工具。
写在最后
如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!
接下来我将对yield
和 Generator
等内容进行详细的介绍,记得关注我哦。