JavaScript 异步编程

1. 为什么 JavaScript 是单线程的?

JavaScript 最初是为了网页交互而设计的。设想如果它是多线程的,一个线程在 DOM 节点上添加内容,另一个线程删除了这个节点,浏览器应该听谁的?这将带来极高的同步复杂性。

因此,JavaScript 选择单线程,意味着它在同一时间只能做一件事。

为了不让耗时任务(如请求接口、读取文件)卡死主线程(导致页面无响应),JavaScript 引入了异步非阻塞机制

2. 异步编程的进化史

我们处理异步的方式经历了几个阶段:

(1) 回调函数 (Callback)

最原始的方案。通过将函数作为参数传递,在任务完成后调用。

缺点:容易陷入“回调地狱”(Callback Hell),层层嵌套导致代码难以维护和调试。

getData(function(a) {
  getMoreData(a, function(b) {
    getMoreData(b, function(c) {
      console.log(c);
    });
  });
});

(2) Promise (ES6)

为了解决回调地狱,Promise 出现了。它代表了一个异步操作的最终结果。

特点

  • 状态管理pending -> fulfilled (成功) 或 rejected (失败)。状态一旦改变不可逆。
  • 链式调用:通过 .then() 将异步步骤串联起来,使代码变扁平。
new Promise((resolve, reject) => {
  setTimeout(() => resolve('Hello'), 1000);
}).then(res => {
  console.log(res); 
});

(3) Generator (生成器)

Generator 函数是可以“暂停”和“恢复”执行的函数。它虽然不是专门为异步设计的,但配合执行器(Executor)可以巧妙地管理异步流程。

  • function* 声明生成器。
  • yield 关键字用来暂停并在暂停处输出一个值。
  • .next() 方法用来恢复执行。

基本用法示例

function* helloGenerator() {
  console.log('开始执行');
  yield '暂停点1'; // 暂停并返回 '暂停点1'
  console.log('恢复执行');
  yield '暂停点2';
}

const gen = helloGenerator();
console.log(gen.next()); // { value: '暂停点1', done: false }
console.log(gen.next()); // { value: '暂停点2', done: false }
console.log(gen.next()); // { value: undefined, done: true }

在 async/await 出现之前,著名的 co 库就是利用 Generator 实现了类似于同步写法的异步控制。

(4) Async/Await (ES8)

这是目前的终极解决方案。简而言之,Async/Await 就是 Generator + Promise 的语法糖

它让异步代码看起来完全像同步代码,极大地提高了可读性。

  • async 函数隐式返回一个 Promise。
  • await 会暂停代码执行,直到等待的 Promise 解决(resolved)。
  • 可以使用标准的 try...catch 进行错误捕获。
async function fetchData() {
  try {
    const res = await someAsyncOperation();
    console.log(res);
  } catch (error) {
    console.error(error);
  }
}

3. Promise 核心 API

除了基础的 then/catch,掌握这些静态方法很重要:

  • Promise.all([p1, p2]): “并”。所有都成功才算成功,有一个失败就立即失败。
  • Promise.race([p1, p2]): “赛跑”。哪个结果跑得快(无论成功失败)就用哪个。
  • Promise.allSettled([p1, p2]) (ES2020): “汇报总结”。等所有都结束,返回每个任务的状态和结果(不会因为一个失败就中断)。
  • Promise.any([p1, p2]) (ES2021): “求胜”。只要有一个成功这就成了;只有全部失败才返回失败。

4. 核心机制:事件循环 (Event Loop)

因为是单线程,JS 依靠事件循环来调度任务。运行时分为:调用栈 (Call Stack)任务队列 (Task Queue)事件循环 (Event Loop)

宏任务 vs 微任务

  • 宏任务 (Macrotask)
    • 整体脚本 (script)
    • setTimeout / setInterval
    • I/O 操作, UI 渲染
  • 微任务 (Microtask)
    • Promise.then (注意是 callback 部分)
    • process.nextTick (Node.js)
    • MutationObserver

循环流程 (非常重要)

  1. 执行同步代码(这本身算第一个宏任务)。
  2. 同步代码执行完,清空微任务队列(所有积攒的微任务依次执行)。
  3. 尝试进行 UI 渲染。
  4. 取出下一个宏任务执行。
  5. 重复步骤 2-4。

(口诀:宏任务走一个,微任务走一队)

5. 高频面试题

Q: 宏任务和微任务的区别?执行顺序?

:微任务优先级更高(在当前回合结尾插队执行)。 每次宏任务执行完,必须清空当下的微任务队列,才会去执行下一个宏任务。

Q:async/await 原理是什么?

:它是 Generator 的语法糖。Generator 可以暂停/恢复,配合一个自动执行器(auto runner),在 yield 一个 Promise 时暂停,等 Promise resolve 后自动调用 next() 并把结果传回,从而通过同步的写法完成异步流程。

Q: 手写 Promise 的思路?

(核心是状态管理 + 发布订阅模式。具体见 q-promise-implementation.md)

Q: 为什么0.1 + 0.2 !== 0.3

(涉及浮点数精度问题,见 q-floating-precision.md)