Day 3|Node.js 异步模型 & Promise / async-await(Part 1)

Part 1 核心概念


一、同步 vs 异步到底差在哪

1️⃣ 同步(Synchronous)

特点:排队,一个一个来,前面不完事,后面全等着

javascript 复制代码
const data = fs.readFileSync('a.txt')
console.log(data)
console.log('done')
bash 复制代码
...文件内容
done

执行顺序是铁板一块

  1. 读文件(阻塞)

  2. 打印 data

  3. 打印 done

📌 问题不在慢,在"卡"

  • CPU 空着

  • 线程被占死

  • 不能处理别的请求

2️⃣ 异步(Asynchronous)

特点:我先登记任务,你忙你的,完事我通知你

javascript 复制代码
fs.readFile('a.txt', (err, data) => {
  console.log(data)
})
console.log('done')
bash 复制代码
done
...文件内容

执行顺序:

  1. 把"读文件任务"交出去

  2. 立刻 执行 console.log('done')

  3. 文件读完 → 回调被调用

📌 重点

异步 ≠ 并行

异步 = 不等结果,先干别的


二、回调地狱为什么会出现?

本质原因只有一句话:

异步操作的"下一步逻辑",只能写在回调里

假设你要做三件事:

  1. 读文件

  2. 处理内容

  3. 再写文件

javascript 复制代码
readFile(a, (err, data) => {
  process(data, (err, result) => {
    writeFile(b, result, (err) => {
      console.log('done')
    })
  })
})

回调地狱的 3 大问题:

  1. 代码向右偏移,阅读成本爆炸

  2. 错误处理分散(每一层都要 if err)

  3. 逻辑顺序不直观

📌 关键点:

不是"回调不好",而是回调不适合表达"线性流程"


三、Promise 解决了什么问题?

Promise 的本质(非常重要):

Promise 是一个"对未来结果的占位符"

它干了 3 件关键的事:


1️⃣ 把"未来的结果"变成一个对象

javascript 复制代码
const p = readFilePromise('a.txt')

这个 p

  • 现在没结果

  • 将来一定有一个结果 or 错误

  • 状态只会走一次:
    pending → fulfilled / rejected


2️⃣ 把"嵌套"变成"链式"

javascript 复制代码
readFilePromise(a)
  .then(process)
  .then(result => writeFilePromise(b, result))
  .catch(err => console.error(err))

📌 本质变化:

  • 不再"向右写"

  • 改为"向下读"


3️⃣ 把错误处理集中化

javascript 复制代码
.then(...)
.then(...)
.catch(...)

👉 中间任何一步出错,直接跳到 catch


一句话总结 Promise

Promise 把"回调何时执行"这件事,从你脑子里拿走了


四、async / await 的本质

先说结论(很重要):

async / await ≠ 同步执行
它只是把 Promise 写得像同步


1️⃣ async 函数的真实身份

javascript 复制代码
async function foo() {
  return 123
}

等价于:

javascript 复制代码
function foo() {
  return Promise.resolve(123)
}

📌 async 函数一定返回 Promise


2️⃣ await 到底在等什么?

javascript 复制代码
const data = await readFilePromise('a.txt')

本质是:

  • 暂停当前 async 函数

  • 不阻塞线程

  • Promise resolved 后,从这里继续执行

📌 暂停的是:

函数执行权,不是线程


3️⃣ try / catch 能抓异步错误的原因

javascript 复制代码
try {
  await readFilePromise('a.txt')
} catch (e) {
  console.error(e)
}

因为:

  • await 会把 rejected 的 Promise

  • 直接当成 throw


async / await 的真正价值

让异步逻辑按"人类的思维顺序"写

而不是改变执行模型。


五、那句关键的话,真正想表达什么?

Node.js 用单线程,靠异步把并发"骗"出来

我们拆开看:

1️⃣ Node.js 真的是单线程吗?

  • JS 执行是单线程

  • 但:

    • I/O 在系统线程池

    • 网络交给操作系统

    • libuv 在背后调度

👉 你写的是单线程代码,底层不是


2️⃣ 并发是怎么"骗"出来的?

假设:

  • 同时 1000 个请求

  • 每个请求都要读数据库

传统同步模型:

  • 一个线程卡一个请求

  • 线程数 = 并发数

  • 爆内存

Node.js:

  • 所有请求共用一个 JS 线程

  • I/O 全部异步丢出去

  • 完事再通知回来

📌 等待 I/O 时,线程不闲着


3️⃣ 为什么这招特别适合 Web?

Web 请求的特点:

  • 90% 时间在等 I/O

  • 真正算逻辑的时间很短

👉 Node.js 把"等"的时间榨干了


六、一句话终极总结

  • 同步:人等机器

  • 异步:机器等人

  • 回调:能跑,但难维护

  • Promise:管理未来

  • async/await:管理人类心智负担

  • Node.js:把等待变成武器


七、通过第二回调地狱的列子结合后面的第三和第四分析

javascript 复制代码
readFile(a, (err, data) => {
  process(data, (err, result) => {
    writeFile(b, result, (err) => {
      console.log('done')
    })
  })
})

为什么"下一步逻辑只能写在回调里"?

先忘掉 JS,想一件更生活化的事。

场景类比

你让同事帮你做一件事:

"你把文件读完,告诉我内容,我好继续处理。"

问题是:

👉 你不知道他什么时候读完

所以你只能说:

"你读完了,来敲我一下。"

这个"敲我一下",就是 callback

在代码里发生了什么?

javascript 复制代码
readFile(a, callback)

这句代码真正的含义是:

  • 我现在 拿不到 data

  • data 未来才会出现

  • 那我只能把"用 data 做什么"提前交出去

📌 所以:

回调不是风格选择,而是时间问题逼出来的结构

为什么一有"多步异步",就必然嵌套?

这个例子,其实是一个严格的因果链

  1. process 必须等 readFile

  2. writeFile 必须等 process

时间线是这样的:

bash 复制代码
readFile 完成
  ↓
process 完成
  ↓
writeFile 完成

而在回调模型里,唯一能确定"上一步已完成"的地方,只有一个:

上一步的回调函数里

于是结构自然变成:

javascript 复制代码
readFile(() => {
  process(() => {
    writeFile(() => {})
  })
})

📌 重点:

不是你想嵌套

是"完成时机"逼你嵌套


代码为什么会"向右生长"?

这是个结构性问题,不是格式问题

回调的代码结构是:

javascript 复制代码
A(() => {
  B(() => {
    C(() => {
      D()
    })
  })
})

无法把它写成:

javascript 复制代码
A()
B()
C()
D()

因为:

  • A 返回时,B 需要的数据还不存在

  • B 根本没资格执行

📌 所以缩进不是"坏习惯",而是时间嵌套的视觉化


错误处理为什么会碎一地?

同步代码的错误流

javascript 复制代码
try {
  A()
  B()
  C()
} catch (e) {
  handle(e)
}

原因很简单:

  • 错误沿着调用栈向上冒泡

回调世界的问题

javascript 复制代码
readFile((err, data) => {
  if (err) return handle(err)

  process(data, (err, result) => {
    if (err) return handle(err)

    writeFile(result, (err) => {
      if (err) return handle(err)
    })
  })
})

为什么不能统一 catch?

📌 因为:

回调执行时,最初的调用栈早就没了

错误根本"爬不回去"。


最被忽视的一个问题:逻辑被"打散"了

你脑子里的逻辑是:

读 → 处理 → 写 → 完成

但回调代码长这样:

  • 读文件:第 1 行

  • 处理逻辑:第 5 行

  • 写文件:第 9 行

  • 完成:第 12 行

📌 逻辑顺序 ≠ 代码顺序

你在读代码时,必须:

  • 跟着缩进跳来跳去

  • 在脑子里重建时间线

这就是为什么:

回调地狱消耗的不是 CPU,是人的工作记忆


为什么说"回调不适合表达线性流程"?

回调最适合的场景其实是:

  • 事件监听

  • 一次性通知

  • 不关心顺序的异步结果

比如:

javascript 复制代码
socket.on('data', data => {})
button.onclick = () => {}

📌 这里没有"下一步必须依赖上一步"的线性关系。


但你的例子是:

  • 有顺序

  • 有依赖

  • 有失败中断

这本质是:

一个异步的"流程"

而回调:

  • 没有流程控制能力

  • 只有"发生时通知"


一句话压轴

回调的问题不是"写法丑"

而是它把"时间"塞进了"结构"里

Promise / async-await 做的唯一一件事就是:

把"时间关系"从代码结构里解放出来


八、将第二回调地狱例子 一步步重构成 Promise 再重构成 async/await

第一步:原始形态(回调地狱)

javascript 复制代码
readFile(a, (err, data) => {
  if (err) return handle(err)

  process(data, (err, result) => {
    if (err) return handle(err)

    writeFile(b, result, (err) => {
      if (err) return handle(err)

      console.log('done')
    })
  })
})

执行时间线(脑补)

bash 复制代码
readFile 开始
readFile 完成 → 进入回调
  process 开始
  process 完成 → 进入回调
    writeFile 开始
    writeFile 完成 → 进入回调
      done

📌 时间线清楚,但代码结构 = 时间嵌套


第二步:Promise 化(时间不再靠缩进表达)

1️⃣ 先把每一步变成 Promise
javascript 复制代码
function readFileP(a) {
  return new Promise((resolve, reject) => {
    readFile(a, (err, data) => {
      if (err) reject(err)
      else resolve(data)
    })
  })
}

function processP(data) {
  return new Promise((resolve, reject) => {
    process(data, (err, result) => {
      if (err) reject(err)
      else resolve(result)
    })
  })
}

function writeFileP(b, result) {
  return new Promise((resolve, reject) => {
    writeFile(b, result, err => {
      if (err) reject(err)
      else resolve()
    })
  })
}

📌 这一刀的本质是:

把"完成时通知"升级成"状态对象"


2️⃣ 用 then 串流程
javascript 复制代码
readFileP(a)
  .then(data => processP(data))
  .then(result => writeFileP(b, result))
  .then(() => {
    console.log('done')
  })
  .catch(err => handle(err))

完整版

javascript 复制代码
function readFileP(a) {
  return new Promise((resolve, reject) => {
    readFile(a, (err, data) => {
      if (err) reject(err)
      else resolve(data)
    })
  })
}

function processP(data) {
  return new Promise((resolve, reject) => {
    process(data, (err, result) => {
      if (err) reject(err)
      else resolve(result)
    })
  })
}

function writeFileP(b, result) {
  return new Promise((resolve, reject) => {
    writeFile(b, result, err => {
      if (err) reject(err)
      else resolve()
    })
  })
}

readFileP(a)
  .then(data => processP(data))
  .then(result => writeFileP(b, result))
  .then(() => {
    console.log('done')
  })
  .catch(err => handle(err))

发生了什么?

  • 没有嵌套

  • 错误集中

  • 流程顺序一眼可见

📌 但你可能已经感觉到了:

可读性提升了,但 still 有点"函数味"


第三步:async / await

javascript 复制代码
async function main() {
  try {
    const data = await readFileP(a)
    const result = await processP(data)
    await writeFileP(b, result)
    console.log('done')
  } catch (err) {
    handle(err)
  }
}

main()

现在你的代码长得像什么?

👉 同步代码

但我们再强调一次:

执行模型完全没变


第四步:执行模型对比

async / await 实际发生的是:

javascript 复制代码
const p1 = readFileP(a)
p1.then(data => {
  const p2 = processP(data)
  p2.then(result => {
    const p3 = writeFileP(b, result)
    p3.then(() => {
      console.log('done')
    })
  })
})

📌 await 只是把 then 藏起来了


第五步:为什么 async/await 能"治愈"回调地狱?

回调的问题

  • 时间关系 = 代码结构

  • 控制流分散在回调函数里

async/await 的改变

  • 时间关系 = Promise 状态

  • 控制流 = 语言级别的顺序语义

换句话说:

回调:

👉「你完成了,我再告诉你我要干嘛
Promise / await:

👉「你先干,我在这等着


第六步:最重要的一刀

async/await 解决的是:

❌ 异步

❌ 并发

❌ 性能

它真正解决的是:

人脑的栈深限制

你不再需要在脑子里模拟:

  • 哪一层回调

  • 哪一层返回

  • 哪一层报错


第七步:送你一句工程级总结

回调是"事件通知模型"

Promise 是"状态机"

async/await 是"状态机的顺序视图"


相关推荐
全栈前端老曹10 小时前
【MongoDB】Node.js 集成 —— Mongoose ORM、Schema 设计、Model 操作
前端·javascript·数据库·mongodb·node.js·nosql·全栈
行者无疆_ty12 小时前
什么是Node.js,跟OpenCode/OpenClaw有什么关系?
人工智能·node.js·openclaw
-凌凌漆-13 小时前
【npm】npm的-D选项介绍
前端·npm·node.js
lucky670713 小时前
Windows 上彻底卸载 Node.js
windows·node.js
Android系统攻城狮14 小时前
鸿蒙系统Openharmony5.1.0系统之解决编译时:Node.js版本不匹配问题(二)
node.js·鸿蒙系统·openharmony·编译问题·5.1
清山博客15 小时前
OpenCV 人脸识别和比对工具
前端·webpack·node.js
何中应17 小时前
nvm安装使用
前端·node.js·开发工具
何中应18 小时前
MindMap部署
前端·node.js
37方寸19 小时前
前端基础知识(Node.js)
前端·node.js