status
type
tags
category
slug
summary
date
finished_date
icon
password
什么是异步编程
假设你正在准备晚餐,如果以同步的方式工作:你必须等鸡肉烤好后才能去煮意大利面。
但为了提高效率,可以通过使用异步操作:将鸡肉放进烤箱开始烤后设置一个计时器,并在在等待鸡肉的过程中,开始准备意大利面。
为什么 JS 需要异步编程?
- JS 最初被设计为在浏览器中运行,它是单线程的,这意味着它一次只能执行一个任务。在执行长时间操作(如网络请求、文件读写等)时,整个程序将停滞,直到操作完成。
- 为了避免阻塞这个线程,JS 使用了事件循环,允许非阻塞的操作,即异步编程的核心。
游览器的事件循环
Event Loop:一种用来管理多个事件和异步操作的机制
同步和异步任务会进入不同的执行环境
- 同步的进入主线程,即主执行栈 (Call Stack)
- 异步的进入任务队列 (Queue)
主要有两个任务队列
微任务
Micro-tasks
Promise
的.then()
、.catch()
和.finally()
回调(📦详见下文)
async/await
语法中的await
关键字后面的代码
MutationObserver
监听 DOM 变化时的回调函数
宏任务
Macro-tasks
- 定时器事件:
setTimeout
、setInterval
- 渲染事件:解析 DOM、计算布局、绘制
- 交互事件:如鼠标点击、键盘事件
- I/O 操作:网络请求、文件读写
事件循环的每个循环中,同步任务 → 微任务 → 宏任务
例子
输出顺序 1 → 3 → 2
(即使延迟时间为 0,回调也只能在所有同步代码执行完毕后。)
Node.js 的事件循环
Node.js 使用 C 语言构建的 Libuv 库实现 Event Loop ,且涉及更多任务队列
process.nextTick
会在当前操作完成后立即执行,并且优先于事件循环的任何阶段,包括微任务队列。
- 适用于希望在当前操作之后、事件循环的下一轮开始之前执行代码的场景,在异步操作中保持操作顺序。
微任务
(与游览器相同)
宏任务
分为以下类型,需要按不同优先级执行。
- Timers:
setTimeout
和setInterval
的回调。 - 进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就回调函数,否则就这个阶段。
- Pending Callbacks:处理延迟执行的 I/O 回调。
- Idle, Prepare:内部操作,普通的用户代码不涉及。
- Poll:轮询时间,执行与 I/O 相关的回调。比如服务器的回应、用户移动鼠标等。该阶段的时间较长,如果没有其它异步任务要处理(比如),会一直停留在这。
- 即需要注意的是 poll 阶段的后面不一定是 check 阶段
- Check:
setImmediate
(Node.js 特有函数,JS 本身没有)的回调。
- Close Callbacks:一些关闭的回调,例如
socket.on('close', ...)
。
每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,即该执行的回调函数都执行了,事件循环才会进入下一个阶段。
例 1
(通常)输出顺序 1 → 2
(一定)输出顺序 2 → 1
为什么前者结果是不确定,偶尔会产生 2 → 1?
- Node.js 做不到完全的 0 毫秒。根据官方文档,第二个参数的最小取值会被默认设为 1 毫秒。也就是说,
setTimeout(f, 0)
等同setTimeout(f, 1)
。
- 因此进入事件循环以后,timer 的执行顺序取决于系统当时的状况。
- 如果同步代码的执行时间刚好等于 1 毫秒,那么 setTimeout 已经到时了,所以当进入宏任务时,会先执行 timer,即 setTimeout 优先于 setImmediate 执行。
- 如果同步代码的执行时间稍微大于 1 毫秒,那么 timer 阶段就会跳过,进入 check 阶段,优先先执行 setImmediate 的回调函数。
为什么后者结果是一定的?
- 同步代码的执行时间一定小于 timer 的 5 毫秒,所以宏任务会顺利进入 poll → check 阶段,执行完 setImmediate ,最后才绕回去执行 setTimeout 。
例 2
输出顺序是 6 → 7 → 5 → 1 → 2 → 4 →3
什么是 Promise
Promise 出现前,JS 的异步编程依靠 。
回调函数容易导致代码结构混乱,产生 “回调地狱” (Callback Hell)。
可以想象 Promise 成一个未来某个时刻可能得到结果的承诺。
它有三种状态:
- Pending(待解决):初始状态。
- Fulfilled(已兑现):操作成功。
- Rejected(已拒绝):操作失败。
它有以下特点:
- 不可取消:如果有一个长时间运行的异步操作,无法中途停止。
- 错误处理:如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 单次解决: Promise 只能解决或拒绝一次,它们无法处理重复发生的事件。
基本构造
- 如果
resolve
传入的是非 Promise 值,则Promise
实例是fulfilled
状态。
- 如果
resolve
传入了另一个 Promise,当前的 Promise 将会 (adopt) 这个传入的 Promise,即等待传入的 Promise 完成状态后,采用传入 Promise 的状态(fulfilled
或rejected
)。
链式调用
适合处理简单的异步操作,或者当需要更精细的控制 Promise 链。
then
方法最多可以接受两个参数(第二个参数只捕获到它之前的 Promise 错误)
catch
能够捕获之前所有 Promise 链中的错误(catch 接收的也是一个 Promise)
链式调用怎么实现的?
- 每个
.then()
方法都返回一个新的 Promise 对象,这个新的 Promise 对象可以用来连接下一个.then()
调用,这样就形成了链式调用。 - 如果某个
then()
回调函数没有显式返回值,那么它实际上返回的是 undefined。
- Promise 内部使用了微任务来异步执行
.then()
方法的回调函数。这意味着每个.then()
中的回调函数会等待当前 JS 执行栈清空后执行,这样可以确保异步操作按照正确的顺序执行。
Async / Await
并发处理
Concurrency methods
Promise.all()
:当有多个异步操作需要执行,并希望等待它们完成时,它允许同时处理多个 Promise 对象,然后返回一个 Promise 对象。
Promise.race()
:在一组 Promise 中的解决或拒绝时就解决或拒绝,返回一个 Promise。
Promise.any()
:返回一组 Promise 中解决的结果。如果所有 Promise 都拒绝,它也拒绝。