JavaScript 单线程原理与异步编程机制

为什么 JavaScript 是单线程?

JavaScript 被设计成单线程,简单来说就是 —— 浏览器里干活儿只能一个接一个排着队来,没法同时多开窗口摸鱼。

举个栗子:

你点按钮 → 网页要弹个提示 → 这时候如果网页还在加载数据 → 弹提示就得等加载完 → 单线程 = 一次只能干一件事

为啥这么设计?

  • 最初网页交互简单(填表单、点按钮),单线程够用。
  • 避免多线程打架(比如两个线程同时改同一个按钮的状态)。

单线程的优点:

  • 开发简单:避免了多线程中的数据竞争、死锁等复杂问题。
  • 调试方便:执行顺序清晰明确,便于排查问题。
  • 适合 I/O 密集型任务:大多数 JS 任务(如事件处理、请求响应)并不需要多核计算资源。

单线程的缺点:

  • 阻塞风险高:一旦有耗时操作(如大数据计算、死循环等),会卡住主线程,导致页面卡顿或无响应。
  • 无法利用多核 CPU:在默认模式下,不能并行计算,浪费了现代多核处理器的能力。

JavaScript 如何实现高并发与多线程?

虽然 JS 是单线程执行模型,但通过浏览器或 Node.js 提供的机制,我们可以实现“伪并发”或“多线程模拟”,主要方式如下:

✅ 异步操作(等加载时先干别的)

  • 原理:任务被挂起,等待资源时让出主线程,通过事件队列机制在任务完成后重新调度执行。
  • 常用方式
    • setTimeout / setInterval
    • Promise
    • async/await
    • Ajax / Fetch API

✅ Web Worker(开小号偷偷干活)

  • 开启一个独立的线程运行 JS 脚本,不影响主线程。
  • 适用于大计算任务、离线数据预处理等。
  • 与主线程通信使用 postMessage() / onmessage
// main.js
const worker = new Worker("worker.js")
worker.postMessage("开始计算")
worker.onmessage = (e) => {
  console.log("子线程结果:", e.data)
}
// worker.js
onmessage = function (e) {
  // 执行密集任务
  let sum = 0
  for (let i = 0; i < 1e8; i++) sum += i
  postMessage(sum)
}

✅ Node.js 中的 Worker Threads

  • 使用 worker_threads 模块在后端实现多线程能力,适合 CPU 密集型场景。

✅ 任务拆碎(把大活切成小碎活穿插着做)

  • 利用 requestIdleCallbacksetTimeout 分片处理数据,减少卡顿。

JavaScript 的事件循环(Event Loop)机制

怎么理解 Event Loop

事件循环就是:
主线程不断地问一句话:
“现在有没有能执行的任务?有就执行一个。”

问完 → 执行 → 再问 → 再执行
一直循环,这就是 Event Loop。


Event Loop 里都有哪些“角色”?

可以把 JS 运行环境想成一个小剧场


调用栈(Call Stack)——正在演的戏

  • 存放正在执行的同步代码
  • 后进先出(LIFO)
function a() {
  b()
}
function b() {
  console.log("b")
}
a()

执行顺序非常直接:

a 入栈 → b 入栈 → b 出栈 → a 出栈

只要调用栈没清空,后面的事都得等着。


Web APIs —— 后台工作人员

浏览器提供的一些“外挂能力”,比如:

  • setTimeout
  • setInterval
  • DOM 事件
  • fetch / Ajax
  • requestAnimationFrame

特点是:

这些不是 JS 引擎自己干的,而是交给浏览器后台去做。

JS 只负责:

  • 注册
  • 回调

任务队列(排队候场)

当 Web API 把事情干完后:

不会直接打断当前代码,而是把回调丢进队列排队。

而这个队列,其实分为两种(重点来了)。


宏任务 & 微任务(Event Loop 核心)

宏任务(Macrotask)

常见宏任务包括:

  • setTimeout
  • setInterval
  • DOM 事件回调
  • requestAnimationFrame
  • MessageChannel

特点:

一次事件循环,只会取一个宏任务执行。


微任务(Microtask)

常见微任务包括:

  • Promise.then / catch / finally
  • queueMicrotask
  • MutationObserver

特点:

优先级比宏任务高,而且会被一次性清空。


执行顺序口诀

一轮事件循环:

  1. 执行一个宏任务
  2. 清空所有微任务
  3. 进行页面渲染
  4. 进入下一轮循环

用代码直观看执行顺序

console.log("A")

setTimeout(() => {
  console.log("B")
}, 0)

Promise.resolve().then(() => {
  console.log("C")
})

console.log("D")

输出顺序是:

A
D
C
B
为什么?
  1. 同步代码先执行:A → D
  2. Promise.then 是微任务 → 立即执行
  3. setTimeout 是宏任务 → 下一轮事件循环

Event Loop 和页面卡顿的关系

为什么死循环会卡死页面?
while (true) {}

因为:

  • 调用栈永远不空
  • 事件循环无法进入下一轮
  • 渲染、点击、定时器全部得不到执行

页面直接“假死”。


Event Loop 与 requestAnimationFrame

requestAnimationFrame 有个特殊位置:

  • 渲染之前执行
  • 跟随浏览器刷新频率(通常 60fps)
  • 页面不可见时会自动暂停

大致顺序是:

宏任务
→ 微任务
→ requestAnimationFrame
→ 页面渲染
→ 下一轮

所以它非常适合:

  • 动画
  • 数字滚动
  • 倒计时展示(UI 层)

但不适合:

  • 精准计时
  • 业务逻辑判断

JavaScript 的事件循环,本质是:

在单线程模型下,
通过任务队列和优先级机制,
协调同步执行、异步回调和页面渲染。

你可以记住这张关系表:

模块 作用
Call Stack 执行同步代码
Web APIs 提供异步能力
宏任务 定时器、事件
微任务 Promise 回调
Event Loop 统一调度
rAF 渲染前执行

异步与同步的区别

同步(Synchronous)

  • 执行顺序严格,必须等待上一个任务完成后才能执行下一个。
  • 阻塞主线程。
console.log("A")
document.querySelector("button").click() // 阻塞直到点击
console.log("B")

异步(Asynchronous)

  • 后台处理任务,不阻塞主线程,通过回调或事件通知结果。
console.log("A")
setTimeout(() => console.log("B"), 1000)
console.log("C")
// 输出顺序:A -> C -> B

Promise、async 和 await 的理解与使用

Promise

  • 用于封装一个异步操作,避免回调地狱。
  • 有三种状态:pending(等待中)、fulfilled(已完成)、rejected(已拒绝)
  • 通过 .then() / .catch() 链式处理结果。
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true
      success ? resolve("数据加载成功") : reject("失败")
    }, 1000)
  })
}

fetchData()
  .then((data) => console.log(data))
  .catch((err) => console.error(err))

async/await

  • Promise 的语法糖,让异步代码写起来像同步代码。
  • 只能在 async 函数中使用。
  • 使用 try/catch 更方便地处理异常。
async function getData() {
  try {
    const data = await fetchData()
    console.log("结果:", data)
  } catch (err) {
    console.error("出错了:", err)
  }
}

getData()

总结

技术/特性 描述
单线程模型 JS 默认仅一个主线程,任务顺序执行
异步操作 不阻塞主线程,通过事件队列执行回调
Web Worker 浏览器中模拟多线程,适合重任务
Node WorkerThreads 后端的多线程计算方案
任务拆分 将大任务拆成小块,分帧执行减轻压力
Promise 管理异步逻辑,避免回调地狱
async/await 让异步代码更像同步,提升可读性

总之,单线程就像收银台只有一个店员,但现代网页用各种办法让这个店员手脚麻利到飞起。

posted @ 2025-04-16 22:37  幼儿园技术家  阅读(191)  评论(0)    收藏  举报