Day 3|Node.js 事件循环 & 异步模型深入(Part 2,3)

Part 2 Promise 全面掌握(从"会用"到"用对")


🎯 目标

不是会写:

javascript 复制代码
new Promise(...)

而是:

  • 能判断 Promise 是串行还是并行

  • 能看懂 Promise 链执行顺序

  • 不写出隐形串行代码

  • 能设计任务依赖关系

一句话:

Promise 是"未来关系的管理器"


一、Promise 本质再确认

Promise 是一个对象,代表:

一个未来一定会完成或失败的结果

三种状态:

  • pending

  • fulfilled

  • rejected

两个铁律:

  1. 状态只能改变一次

  2. 一旦改变不可逆

英文 推荐中文理解 本质含义
pending 未决 还没出结果
fulfilled 已兑现 成功了
rejected 已拒绝 失败了

再补一个关键点(很多人忽略)

状态转换只能这样:

pending → fulfilled

pending → rejected

不能:

fulfilled → rejected ❌

rejected → fulfilled ❌

一旦确定,终身不变。


二、then 链的核心机制

javascript 复制代码
readFileP(a)
  .then(data => processP(data))
  .then(result => writeFileP(b, result))
  .catch(err => handle(err))

关键理解:

then 一定返回一个新的 Promise

规则:

  • return 普通值 → 自动 Promise.resolve

  • throw → 自动 Promise.reject

  • return Promise → 接管后续链条

这就是为什么:

  • 不需要嵌套

  • 不需要层层回调

方法 状态 用途
Promise.resolve(value) fulfilled 包装成功结果
Promise.reject(error) rejected 主动制造失败

三、Promise 的错误传播机制

javascript 复制代码
readFileP(a)
  .then(() => {
    throw new Error('boom')
  })
  .catch(err => {
    console.log(err)
  })

理解:

then 里的 throw 等价于 return Promise.reject()

错误会沿着 then 链向下传递,直到被 catch 捕获。

这和同步 throw 不一样。

举个例子

javascript 复制代码
function testPromiseError() {
  console.log('1. start')

  Promise.resolve()
    .then(() => {
      console.log('2. inside then')
      throw new Error('boom')
    })
    .then(() => {
      console.log('3. will not run')
    })
    .catch(err => {
      console.log('4. caught:', err.message)
    })

  console.log('5. end')
}

testPromiseError()

输出顺序

bash 复制代码
1. start
5. end
2. inside then
4. caught: boom

1.同步代码先执行

bash 复制代码
1
5

2.throw 之后的 then 被跳过

这句不会执行:

javascript 复制代码
console.log('3. will not run')

因为:

throw 会把当前 Promise 变成 rejected

然后直接跳到最近的 catch


四、Promise 并发控制(最关键)


1️⃣ Promise.all

特点:

  • 并行执行

  • 任意失败 → 整体失败

  • 返回数组(顺序不变)

javascript 复制代码
await Promise.all([p1, p2, p3])

适合:

  • 任务互不依赖

  • 成败同生共死

⚠️ 常见错误:

javascript 复制代码
await Promise.all([
  await p1,
  await p2
])

这会退化成串行。


2️⃣ Promise.allSettled

  • 所有任务跑完

  • 不管成功失败

  • 返回状态数组

适合:

  • 批处理

  • 日志扫描

  • 允许部分失败


3️⃣ Promise.race

  • 谁先结束用谁

  • 常用于超时控制


4️⃣ Promise.any

  • 第一个成功的结果

  • 全失败才 reject


五、Promise 执行顺序基础(为事件循环铺垫)

javascript 复制代码
console.log('1')

Promise.resolve().then(() => {
  console.log('2')
})

console.log('3')

输出:

bash 复制代码
1
3
2

原因:

  • then 属于微任务

  • 当前同步代码执行完后才执行


六、必须掌握的心法

  1. Promise 不改变异步本质

  2. then 只是"未来回调的升级版"

  3. 并发取决于 Promise 创建时机

  4. await 写在哪,性能差在哪


七、实战练习

练习 1:

  • 并行读取 3 个文件

  • 任意失败 → 整体失败

  • 打印耗时

参考答案

创建js practice1.js 同路径分别添加三个文本文件 1.txt 2.txt 3.txt

javascript 复制代码
const fs = require('fs/promises')
const path = require('path')

async function main() {
  const start = Date.now()

  try {
    // 1️⃣ 先创建 Promise(关键:不要立刻 await)
    const p1 = fs.readFile(path.join(__dirname, '1.txt'), 'utf-8')
    const p2 = fs.readFile(path.join(__dirname, '2.txt'), 'utf-8')
    const p3 = fs.readFile(path.join(__dirname, '3.txt'), 'utf-8')

    // 2️⃣ 并行等待
    const [file1, file2, file3] = await Promise.all([p1, p2, p3])

    console.log('===== file1 =====')
    console.log(file1)

    console.log('===== file2 =====')
    console.log(file2)

    console.log('===== file3 =====')
    console.log(file3)

    const end = Date.now()
    console.log('\n总耗时:', end - start, 'ms')
  } catch (err) {
    console.error('读取失败:', err.message)
  }
}

main()

返回结果

bash 复制代码
===== file1 =====
。。。。
===== file2 =====
。。。
===== file3 =====
。。

总耗时: 5 ms

可把其中一个文件名改错,比如:

javascript 复制代码
const p3 = fs.readFile(path.join(__dirname, '3333.txt'), 'utf-8')

则结果为

bash 复制代码
读取失败: ENOENT: no such file or directory, open '文件路径'

练习 2:

  • 使用 allSettled

  • 输出每个文件成功或失败

参考答案

javascript 复制代码
const fs = require('fs/promises')
const path = require('path')

async function main() {
  const start = Date.now()

  try {
    // 1️⃣ 先创建 Promise(关键:不要立刻 await)
    const p1 = fs.readFile(path.join(__dirname, '1.txt'), 'utf-8')
    const p2 = fs.readFile(path.join(__dirname, '2.txt'), 'utf-8')
    const p3 = fs.readFile(path.join(__dirname, '3333.txt'), 'utf-8')

    // 2️⃣ 并行等待
    const [file1, file2, file3] = await Promise.allSettled([p1, p2, p3])

    console.log('===== file1 =====')
    console.log(file1)

    console.log('===== file2 =====')
    console.log(file2)

    console.log('===== file3 =====')
    console.log(file3)

    const end = Date.now()
    console.log('\n总耗时:', end - start, 'ms')
  } catch (err) {
    console.error('读取失败:', err.message)
  }
}

main()

输出结果

bash 复制代码
===== file1 =====
{
  status: 'fulfilled',
  value: '文件内容'
}
===== file2 =====
{
  status: 'fulfilled',
  value: '文件内容'
}
===== file3 =====
{
  status: 'rejected',
  reason: Error: ENOENT: no such file or directory, open '文件地址'
      at async open (node:internal/fs/promises:639:25)
      at async Object.readFile (node:internal/fs/promises:1243:14)
      at async Promise.allSettled (index 2)
      at async main (报错行位置) {
    errno: -4058,
    code: 'ENOENT',
    syscall: 'open',
    path: '文件地址'
  }
}

总耗时: 6 ms

练习 3:

实现:

javascript 复制代码
withTimeout(promise, ms)

withTimeout(promise, ms) 是什么?

不是 Node 内置函数

它是我们自己封装的一个工具函数,用来:

给一个 Promise 加"超时限制"

意思是:

  • 如果 promise 在规定时间内完成 → 返回结果

  • 如果超过时间还没完成 → 直接失败

为什么需要它?

现实后端开发中:

  • 调数据库

  • 调第三方接口

  • 调 AI 服务

  • 调微服务

都可能 卡住不返回

如果不做超时控制,那你的接口就会一直挂着。

参考答案

javascript 复制代码
function slowTask(ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('任务完成')
    }, ms)
  })
}

function withTimeout(promise, ms) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error('请求超时'))
    }, ms)
  })

  return Promise.race([promise, timeoutPromise])
}

async function main() {
  try {
    const result = await withTimeout(slowTask(3000), 2000)
    console.log(result)
  } catch (err) {
    console.error('错误:', err.message)
  }
}

main()

输出结果:错误: 请求超时

javascript 复制代码
async function main() {
  try {
    const result = await withTimeout(slowTask(1000), 2000)
    console.log(result)
  } catch (err) {
    console.error('错误:', err.message)
  }
}

输出结果:任务完成

时间线:

bash 复制代码
0s → 两个同时开始
2s → timeout 先 reject
3s → slowTask 才 resolve(但已经没用了)

⚠️ 注意:

即使 timeout 先返回,slowTask 仍然在后台运行。

这就是 race 的特点。

在真实服务器中:

你不仅要 timeout

你还要:

  • 取消数据库查询

  • 中断 HTTP 请求

  • 释放资源

如果 slowTask 永远不 resolve 也不 reject,会发生什么?

程序会永远卡住在这里

因为:

  • Promise 永远是 pending

  • await 会一直等

  • 后面的代码永远不会执行

在服务器里:

  • 请求不会返回

  • 连接不会关闭

  • 资源被占用

  • 内存泄漏风险

这就是所谓的:

悬空 Promise(Hanging Promise)

如果一个 Promise 永远 pending:

  • await 会永远卡住

  • then 永远不会执行

  • catch 也不会执行

  • 程序会挂在那

Promise 可以取消吗?

标准答案:

原生 Promise 不支持取消,只能通过额外机制(如 AbortController)实现。


Part 3 事件循环基础结构


事件循环的简化模型:

bash 复制代码
执行同步代码
↓
清空微任务队列
↓
执行一个宏任务
↓
清空微任务队列
↓
循环

一、宏任务 vs 微任务(重点)


🟦 宏任务(Macrotask)

常见:

  • setTimeout

  • setInterval

  • setImmediate

  • I/O

  • 整个 script

在 I/O 回调里,setImmediate 会比 setTimeout 更早执行。


🟩 微任务(Microtask)

常见:

  • Promise.then

  • catch

  • finally

  • async/await 后续代码

  • queueMicrotask

Node 还有一个特殊:

  • process.nextTick(优先级更高)

记住核心规则:

同步 → 微任务 → 宏任务


列子1

javascript 复制代码
console.log('1')

setTimeout(() => {
  console.log('2')
}, 0)

Promise.resolve().then(() => {
  console.log('3')
})

console.log('4')

输出

bash 复制代码
1
4
3
2

原因:

  • 1 和 4 是同步

  • Promise.then 是微任务

  • setTimeout 是宏任务


例子2

javascript 复制代码
Promise.resolve()
  .then(() => {
    console.log('then1')
  })
  .then(() => {
    console.log('then2')
  })

setTimeout(() => console.log('timeout'))

console.log('end')

输出

bash 复制代码
end
then1
then2
timeout

关键理解:

微任务队列会一次性清空


二、async / await 执行机制

列子

javascript 复制代码
async function test() {
  console.log('1')
  await Promise.resolve()
  console.log('2')
}

test()
console.log('3')

输出

bash 复制代码
1
3
2

解释:

  • await 后面的代码被放入微任务队列

  • 相当于 Promise.then


三、Node 特有 ------ process.nextTick

优先级

bash 复制代码
同步
↓
process.nextTick
↓
Promise 微任务
↓
宏任务

例子

javascript 复制代码
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('promise'))
console.log('sync')

输出

bash 复制代码
sync
nextTick
promise

相关推荐
章丸丸18 小时前
Tube - Video Reactions
react.js·node.js·next.js
折七1 天前
2026 年 Node.js 后端技术选型,为什么我选了 Hono 而不是 NestJS
前端·后端·node.js
我叫唧唧波1 天前
【NodeJS】从入门到进阶
node.js
张3蜂1 天前
Node.js 安装与配置完全指南:从零开始搭建开发环境
node.js
礼拜天没时间.2 天前
Node.js运维部署实战:从0到1开始搭建Node.js运行环境
linux·运维·后端·centos·node.js·sre
等什么君!2 天前
如何正确使用nvm工具管理 node.js
node.js
受打击无法动弹3 天前
Window 10部署openclaw报错node.exe : npm error code 128
npm·node.js·openclaw
全马必破三4 天前
Webpack知识点汇总
前端·webpack·node.js
NEXT064 天前
CommonJS 与 ES Modules的区别
前端·面试·node.js