JavaScript 异步编程:从单线程到异步

105 阅读8分钟

一、进程与线程:JavaScript 的 "工作模式"

要理解 JavaScript 的异步特性,首先得搞懂两个基础概念:进程线程

  • 进程:可以理解为一个 "独立的工厂",是程序运行的基本单位,拥有独立的内存空间。比如打开一个浏览器标签页,就是启动了一个进程。
  • 线程:进程内的 "工人",负责执行具体的代码。一个进程可以有多个线程(多线程),但多个线程共享进程的内存资源。

为什么 JavaScript 是单线程?

JavaScript 被设计为单线程(一个进程里只有一个 "工人"),原因很简单:它最初是为浏览器设计的脚本语言,主要用来操作 DOM。如果有多个线程同时操作 DOM,可能会导致冲突(比如一个线程删除 DOM,另一个线程修改 DOM)。

v8 在执行js代码时, 默认只开启一个线程工作,可以通过操作开启多线程,线程之间通常可以同时工作, 但是有 js 引擎线程和渲染线程是互斥的

单线程意味着:代码只能 "从头到尾依次执行",但这会带来问题 —— 如果遇到耗时操作(比如网络请求),会阻塞后面的代码。为了解决这个问题,JavaScript 引入了同步与异步的执行机制。

二、同步与异步:代码的 "执行节奏"

JavaScript 代码分为两种执行方式,就像现实中的 "立即做" 和 "稍后做":

1. 同步代码

同步代码会立即执行,按顺序一步一步执行,前一句没执行完,后一句不会开始。

let a = 1; // 同步:立即执行,a=1
console.log(a); // 同步:立即执行,打印1

2. 异步代码

异步代码不会立即执行,而是先 "登记" 到任务队列中,等所有同步代码执行完后,再按顺序执行。常见的异步操作有:setTimeoutsetInterval、网络请求(fetch)、DOM 事件(click)等。

let a = 1; // 同步:先执行,a=1

// 异步:登记到任务队列,1秒后执行
setTimeout(() => {
  a = 2;
  console.log(a); // 1秒后打印2
}, 1000);

console.log(a); // 同步:先执行,打印1(此时a还没被修改)

执行顺序解析

  1. 先执行同步代码:a=1 → console.log(a)(输出 1)
  2. 同步代码执行完后,从任务队列中取出异步代码执行:a=2 → console.log(a)(输出 2)

三、Promise:解决 "回调地狱" 的利器

早期处理异步操作依赖回调函数,但多层嵌套会导致 "回调地狱"(代码像金字塔一样嵌套,可读性极差)。例如:

// 回调地狱示例:获取用户信息→获取订单→获取物流,嵌套三层
getUser(userId, (user) => {
  getOrder(user.orderId, (order) => {
    getLogistics(order.logisticsId, (logistics) => {
      console.log(logistics);
    });
  });
});

Promise 的出现就是为了让异步代码更扁平、更易读。

1. Promise 是什么?

Promise 是一个异步操作的 "容器" ,它有三种状态:

  • pending(等待中):初始状态,操作未完成

  • fulfilled(已成功):操作完成,调用 resolve() 触发

  • rejected(已失败):操作出错,调用 reject() 触发

状态一旦改变(从 pending → fulfilled 或 rejected),就不会再变

2. Promise 的基本用法

// 创建一个Promise实例
const promise = new Promise((resolve, reject) => {
  // 异步操作(比如定时器、网络请求)
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("操作成功"); // 成功时调用,状态变为fulfilled
    } else {
      reject("操作失败"); // 失败时调用,状态变为rejected
    }
  }, 1000);
});

// 用then处理成功,catch处理失败
promise
  .then((result) => {
    console.log(result); // 输出"操作成功"
  })
  .catch((error) => {
    console.log(error); // 若失败,输出"操作失败"
  });

3. 链式调用:解决嵌套问题

Promise 的 then 方法会返回一个新的 Promise,因此可以链式调用,让多层异步操作像同步代码一样线性书写。

// 示例:按顺序执行 xq→m→b 三个异步操作
function xq() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("1");
      resolve(); // 完成后通知下一步
    }, 1000);
  });
}

function m() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("2");
      resolve();
    }, 2000);
  });
}

function b() {
  setTimeout(() => {
    console.log("3");
  }, 500);
}

// 链式调用:依次执行
xq()
  .then(() => m()) // xq完成后执行m
  .then(() => b()) // m完成后执行b
  .catch((error) => console.log("出错了:", error));

执行顺序:1(1 秒后)→ 2(再等 2 秒)→ 3(再等 0.5 秒),完美按顺序执行。

image.png

四、Event-loop:JavaScript 的 "事件循环" 机制

JavaScript 单线程如何同时处理同步和异步操作?秘密在于Event-loop(事件循环) ,它像一个 "调度员",不断协调同步和异步任务的执行。

1. 任务队列的分类

异步任务分为两类,优先级不同:

  • 微任务(Microtasks) :优先级高,会在同步代码执行完后立即执行。包括:

    • Promise 的 then/catch/finally
    • process.nextTick(Node.js 特有)
    • MutationObserver(DOM 变化监听)
  • 宏任务(Macrotasks) :优先级低,在所有微任务执行完后才执行。包括:

    • setTimeout/setInterval
    • setImmediate(Node.js 特有)
    • 网络请求(fetch
    • DOM 事件(click/load
    • 脚本执行(整体代码属于第一个宏任务)

2. Event-loop 执行流程

  1. 执行同步代码(属于当前宏任务),遇到异步任务就按类型放入对应的队列(微任务 / 宏任务)。

  2. 同步代码执行完后,清空微任务队列(按顺序执行所有微任务)。

  3. 微任务执行完后,浏览器可能会进行页面渲染

  4. 从宏任务队列中取出第一个任务执行,重复步骤 1-4,形成循环。

简单说:同步 → 微任务 → 渲染 → 宏任务 → 同步...

3. 实例分析:代码执行顺序

console.log(1); // 同步代码(宏任务)

new Promise((resolve) => {
  console.log(2); // Promise构造函数内是同步代码,立即执行
  resolve();
}).then(() => {
  console.log(3); // 微任务
  setTimeout(() => {
    console.log(4); // 宏任务
  }, 0);
});

setTimeout(() => {
  console.log(5); // 宏任务
  setTimeout(() => {
    console.log(6); // 宏任务
  }, 0);
}, 0);

console.log(7); // 同步代码(宏任务)

分步解析

  1. 执行同步代码(宏任务):

    • 打印 1 → 执行 Promise 构造函数(打印 2)→ 打印 7
    • 此时微任务队列有:console.log(3)
    • 宏任务队列有:第一个 setTimeout(打印 5)、then 里的 setTimeout(打印 4)
  2. 同步代码执行完,清空微任务队列:

    • 执行 console.log(3) → 打印 3
    • 此时微任务队列空,宏任务队列新增了 console.log(4)
  3. 渲染页面(假设需要)。

  4. 执行第一个宏任务(打印 5):

    • 打印 5 → 遇到 setTimeout(打印 6),放入宏任务队列。
  5. 重复步骤 1-4:

    • 同步代码(无新同步)→ 微任务队列空 → 执行下一个宏任务(打印 4)→ 打印 4

    • 再取下一个宏任务(打印 6)→ 打印 6

最终输出顺序:1 → 2 → 7 → 3 → 5 → 4 → 6 image.png

五、async/await:异步代码的 "同步写法"

async/await 是 ES2017 引入的语法,基于 Promise,让异步代码看起来像同步代码,可读性更强。

1. 基本用法

  • async:修饰函数,表明该函数是异步的,返回值会自动包装成 Promise

  • await:只能用在 async 函数内,后面跟一个 Promise,会暂停函数执行,等待 Promise 完成后再继续。

// 用async/await改写前面的Promise链式调用
function xq() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("1");
      resolve();
    }, 1000);
  });
}

function m() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("2");
      resolve();
    }, 2000);
  });
}

function b() {
  setTimeout(() => {
    console.log("3");
  }, 500);
}

// async函数:用同步的写法执行异步操作
async function run() {
  await xq(); // 等待xq完成
  await m(); // 等待m完成
  b(); // 最后执行b
}

run(); // 执行结果和链式调用完全一致

image.png

2. 错误处理

await 后的 Promise 如果失败(reject),会抛出错误,可以用 try/catch 捕获:

async function fetchData() {
  try {
    const res = await fetch("https://api.example.com/data"); // 网络请求返回Promise
    const data = await res.json();
    console.log("数据:", data);
  } catch (error) {
    console.log("请求失败:", error); // 捕获所有错误
  }
}

3. 复杂实例:结合 Event-loop

console.log("start");

async function async1() {
  await async2(); // 等待async2完成
  console.log("async1 end"); // 这行属于微任务
}

async function async2() {
  console.log("async2"); // 同步执行
}

async1();

setTimeout(() => {
  console.log("setTimeout"); // 宏任务
}, 0);

new Promise((resolve) => {
  console.log("Promise"); // 同步执行
  resolve();
}).then(() => {
  console.log("Promise then"); // 微任务
});

console.log("end");

执行顺序解析

  1. 同步代码:start → async2(async1 调用 async2)→ Promise → end

  2. 微任务队列:async1 end(await 后的代码)→ Promise then(按顺序执行)

  3. 宏任务队列:setTimeout(最后执行)

输出结果start → async2 → Promise → end → async1 end → Promise then → setTimeout

image.png

V8引擎对已 resolved 的 Promise 使用了 快速路径优化 :

  • 如果 await 等待的 Promise 已经 resolved,后续代码会被延迟到 下一轮微任务
  • 这导致 Promise then (当前微任务)比 async1 end (下一轮微任务)先执行
console.log("start");

async function async1() {
  await async2(); // 等待async2完成
  console.log("async1 end"); // 这行属于微任务
}

async function async2() {
  console.log("async2"); // 同步执行
  return Promise.resolve(); // 返回一个已完成的Promise
}

async1();

setTimeout(() => {
  console.log("setTimeout"); // 宏任务
}, 0);

new Promise((resolve) => {
  console.log("Promise"); // 同步执行
  resolve();
}).then(() => {
  console.log("Promise then"); // 微任务
});

console.log("end");

image.png

总结

JavaScript 异步编程的核心知识点:

  • 单线程:只有一个 "工人",避免 DOM 操作冲突。

  • 同步 vs 异步:同步按顺序执行,异步 "先登记后执行"。

  • Promise:用链式调用解决回调地狱,三种状态控制流程。

  • Event-loop:同步 → 微任务 → 宏任务的循环机制,决定代码执行顺序。

  • async/await:Promise 的语法糖,让异步代码像同步一样易读。

掌握这些概念,就能轻松应对 JavaScript 中的异步场景,写出高效且易维护的代码!