从「Hello World」到 Promise:JavaScript 异步编程的前世今生

50 阅读5分钟

从「Hello World」到 Promise:JavaScript 异步编程的前世今生

为什么你的 console.log(2) 总是迟到?单线程的 JS 是如何“一边煮饭一边打游戏”的?


🌟 你写的代码,JS 真的按顺序执行了吗?

先来看一段看似平平无奇的代码:

console.log(1);
setTimeout(function() {console.log(2);},3000)
console.log(3);

你以为输出是 1 → 2 → 3
错!实际输出是:1 → 3 → (等3秒) → 2

💡 灵魂拷问:JS 不是单线程吗?怎么还能“同时”干两件事?

别急,今天我们就来揭开 JavaScript 异步世界的神秘面纱——从 setTimeoutPromise,再到 fetch 请求 GitHub 成员列表,带你一步步搞懂:JS 是如何在“单线程”的限制下,优雅地处理耗时任务的


🧵 第一章:单线程 ≠ 笨线程 —— Event Loop 的魔法

1.1 为什么 JS 是单线程?

想象一下:
你正在用浏览器看掘金文章,突然点击一个按钮要删除一篇文章。
如果 JS 是多线程,可能一个线程在删文章,另一个线程在编辑同一篇文章——数据直接爆炸💥

所以,JS 选择做“专一的男人”:只有一个主线程,负责执行所有同步代码。

但问题来了:网络请求、文件读取、定时器……这些动辄几百毫秒的操作,难道要卡住整个页面?

答案是:绝不!

1.2 Event Loop:JS 的“任务调度员”

JS 把任务分为两类:

  • 同步任务(Synchronous) :立刻执行,比如 console.log、变量赋值。
  • 异步任务(Asynchronous) :交给浏览器/Web API 去处理,比如 setTimeoutfetchfs.readFile

当遇到异步任务时,JS 主线程会说:

“兄弟,这事你先去后台办,办好了把回调函数塞进‘任务队列’(Task Queue)。我先把手上活干完,回头再看你。”

这个“回头再看”的机制,就叫 Event Loop(事件循环)

执行顺序

  1. 执行所有同步代码(console.log(1) → console.log(3)
  2. 检查任务队列
  3. 如果有已完成的异步任务,执行其回调(console.log(2)

🎯 结论:异步代码不会阻塞主线程,但它的回调一定在同步代码之后执行!


🪄 第二章:Promise —— 把“混乱”变成“流程”

2.1 回调地狱(Callback Hell)的痛

早期 JS 处理异步靠回调函数:

fs.readFile('a.txt', () => {
  fs.readFile('b.txt', () => {
    fs.readFile('c.txt', () => {
      // 噩梦开始...
    });
  });
});

嵌套三层就眼花了?这还只是读文件!网络请求链式调用更是灾难。

Promise:异步的“许诺书”

ES6 引入 Promise,让异步代码看起来像同步

const p = new Promise((resolve) => {
     setTimeout(function() {
     console.log(2);
         resolve();
        },3000)
        })
p.then(() => {
            console.log(3);
        })
        console.log(4);
// 输出:4 → 2 → 3

但如果你手滑把 resolve() 挪到外面:

const p = new Promise((resolve) => {
     setTimeout(function() {
     console.log(2);
     },3000)
        resolve();
     })
p.then(() => {
            console.log(3);
        })
        console.log(4);
// 输出:4 → 3 → (3秒后) → 2

💥 关键区别

  • resolve() 在 setTimeout 内 → Promise 等 3 秒后兑现 → .then() 在 2 后执行
  • resolve() 在外 → Promise 立刻兑现 → .then() 立即排队(微任务),比 setTimeout 更早执行!

这是因为: .then() 的触发只取决于 resolve() 何时被调用,而不是异步操作是否完成!

⚠️ 如果提前 resolve(),而 .then() 需要异步结果(如文件内容、API 数据),就会拿到 undefined,逻辑直接崩坏!

最佳实践resolve() 必须在异步操作真正完成之后调用!

✨ Promise 的三大状态:
  • pending(等待中) :刚创建,还没结果
  • fulfilled(成功) :调用 resolve()
  • rejected(失败) :调用 reject() 或抛出错误
🛠 链式调用 .then().catch()
p
  .then(data => { /* 处理成功 */ })
  .catch(err => { /* 处理失败 */ });

💡 关键点.then().catch() 本身也返回新的 Promise,所以能链式调用!


🌐 第三章:实战!用 fetch 获取 GitHub 成员

来看你提供的代码:

<ul id="memebers"></ul>
    <script>
        fetch('https://api.github.com/orgs/lemoncode/members')
        .then(data => data.json())
        .then(res => {
        document.getElementById('memebers').innerHTML = res.map(item => `<li>${item.login}</li>`).join('');
        })
    </script>

🔍 分解步骤:

  1. fetch() 发起网络请求 → 返回一个 Promise
  2. 第一个 .then() 把响应转为 JSON → 返回新 Promise
  3. 第二个 .then() 拿到数据,渲染到页面

优势:代码清晰、错误可捕获、避免回调地狱!

⚠️ 注意:fetch 默认不 reject HTTP 错误(如 404),需手动检查 res.ok


🧪 Node.js 中手动封装 Promise 的执行细节

来看这段典型 Node.js 代码:

import fs from 'fs';

console.log(1);

const p = new Promise((resolve, reject) => {
  console.log(3); // 👈 同步!立即执行
  fs.readFile('./b.txt', (err, data) => {
    if (err) {
      reject(err);
      return;
    }
    resolve(data.toString()); // 异步兑现 Promise
  });
});

p.then(data => {
  console.log(data, '///////');
}).catch(err => {
  console.log(err, '读取文件失败');
});

console.log(2);

🔍 执行顺序解析:

输出为:1 → 3 → 2 → (文件内容 或 错误信息)

为什么是这个顺序?
  • console.log(1):同步代码,最先执行。

  • 进入 new Promise(...)Promise 的执行器函数(executor)是同步立即执行的,所以 console.log(3) 紧接着输出。

  • fs.readFile 是异步 I/O 操作,调用后立即返回,不阻塞主线程。

  • 主线程继续执行 console.log(2)

  • 当文件读取完成(从硬盘加载到内存),回调函数被放入任务队列。

  • Event Loop 在同步代码执行完毕后,处理该回调:

    • 若成功 → 调用 resolve() → 触发 .then()
    • 若失败 → 调用 reject() → 触发 .catch()

关键知识点

  1. Promise 构造函数内部的 executor 是同步执行的
  2. 真正的异步操作(如 fs.readFile)发生在回调中
  3. .then() / .catch() 的执行时机取决于 resolve/reject 何时被调用,而非 Promise 对象创建时间。

🏁 终极总结:异步编程心法

概念关键理解
单线程JS 只有一个主线程,但可通过 Event Loop 实现“伪并发”
异步任务不阻塞主线程,回调放入任务队列
Promise将异步操作封装成对象,用 .then() 控制流程
async/await(Bonus)基于 Promise 的语法糖,让异步代码像同步一样写

🤓 补充细节

  • Microtask vs MacrotaskPromise.then 属于微任务(microtask),优先级高于 setTimeout(宏任务)。这意味着:

    setTimeout(() => console.log('A'), 0);
    Promise.resolve().then(() => console.log('B'));
    // 输出:B → A
    
  • Promise 构造函数是同步的new Promise(fn) 会立即执行 fn

  • 错误传播:Promise 链中任何一个环节 throw 或 reject,都会跳到最近的 .catch()