当前位置: 首页 > ai >正文

JavaScript 中的并发编程实践与误区:一次深入的探讨

JavaScript,作为一门在Web开发领域占据主导地位的语言,其独特的“单线程”设计常常让人对“并发”这个概念感到困惑:一个单线程的语言,如何实现并发?

其实,JavaScript 的核心是单线程执行模型,这意味着在同一时刻,它一次只能执行一个任务。然而,这并不意味着JavaScript不能“同时”处理多个事情。这里的关键在于理解JavaScript的并发 (Concurrency) 和并行 (Parallelism) 的区别,以及它如何利用事件循环 (Event Loop) 和Web API/Node.js API来模拟并发。

本文将深入探讨JavaScript的并发编程实践,揭示其背后的机制,并纠正一些常见的误区。

一、 理解 JavaScript 的核心:单线程与事件循环

在深入并发之前,我们必须先理解JavaScript的执行环境:

单线程 (Single-Threaded): JavaScript 引擎在单个主线程上执行代码。这意味着任何JavaScript代码块(同步执行的代码)都会按顺序执行,不会因为其他任务而中断。

内存堆 (Heap) 与 调用栈 (Call Stack):

堆 (Heap): 用于存储对象、数组、函数等复杂数据结构的内存区域。

调用栈 (Call Stack): 用于管理函数执行。当函数被调用时,它的执行上下文会被压入栈顶;函数执行完毕后,其上下文从栈顶弹出。

事件循环 (Event Loop):

这是JavaScript实现“并发”的关键机制。事件循环由以下几个部分组成:

调用栈 (Call Stack): 如前所述,执行同步代码的地方。

Web API / Node.js API (环境提供的能力): 这些是JavaScript引擎之外提供的API,例如 setTimeout, fetch, DOM manipulation 等。这些API会在后台线程中执行,而不会阻塞主线程。

回调队列 (Callback Queue / Task Queue): 当Web API(如 setTimeout 的回调)执行完毕时,会将它们的回调函数放入回调队列中。

微任务队列 (Microtask Queue): 包含比宏任务(Callback Queue中的任务)有更高优先级的任务,如 Promise 的 .then()、.catch()、.finally() 的回调,以及 queueMicrotask()。

事件循环的过程:

执行栈: JavaScript引擎首先执行调用栈中的同步代码。

API调用: 当遇到一个需要等待的操作(如 setTimeout(callback, 1000)),主线程不会等待,而是将这个操作交给Web API/Node.js API在后台线程处理。

回调入队: 当后台线程完成任务时,会将对应的回调函数放入宏任务队列(或微任务队列,取决于任务类型)。

事件循环轮询:

当调用栈为空时,事件循环会检查微任务队列。如果存在微任务,它会依次将微任务从队列中取出,推入调用栈执行,直到微任务队列为空。

然后,事件循环会检查宏任务队列。如果存在宏任务,它会从宏任务队列中取出一个任务,推入调用栈执行。

这个检查和执行的过程不断循环,直到程序结束。

为什么这能模拟并发?

尽管JavaScript主线程是单线程的,但通过将耗时操作(如网络请求、定时器)交给环境提供的API在后台线程去执行,主线程就可以继续执行其他同步代码,或者处理下一次事件循环。当后台任务完成后,其回调函数会被安排在事件循环中执行,从而实现了“看起来像同时”进行的效果。

二、 JavaScript 中的并发实践

JavaScript 的并发主要围绕事件循环和其API展开。

1. 定时器 (setTimeout, setInterval)

实践:

setTimeout(callback, delay):延迟 delay 毫秒后执行一次 callback。

setInterval(callback, interval):每隔 interval 毫秒重复执行一次 callback。

<JAVASCRIPT>

console.log("Start");

setTimeout(() => {

console.log("Timeout callback executed after 2 seconds");

}, 2000);

let count = 0;

const intervalId = setInterval(() => {

count++;

console.log("Interval callback executed:", count);

if (count >= 3) {

clearInterval(intervalId); // 清除定时器,避免无限执行

console.log("Interval cleared.");

}

}, 1000);

console.log("End of script, but timeouts/intervals are still scheduled.");

机制: setTimeout 和 setInterval 会将回调函数交给Web API的计时器,由它在后台倒计时。时间到后,回调会被放入宏任务队列,等待事件循环处理。

注意: setInterval 的回调如果在执行过程中超过了间隔时间,可能会导致回调堆叠(如果事件循环处理慢于间隔),或者被跳过(取决于具体实现)。为了防止这种情况,通常在该回调内部手动调用 clearInterval。

2. Promise 与异步操作

Promise 是 ES6 引入的,用于更优雅地处理异步操作。它代表一个异步操作的最终完成(fulfilled)或失败(rejected)。

实践:

fetch API:进行网络请求。

文件操作(Node.js):进行文件读写。

Promise.all, Promise.race 等:组合多个 Promise。

<JAVASCRIPT>

function simulateFetch(url) {

return new Promise((resolve, reject) => {

console.log(`Fetching data from ${url}...`);

setTimeout(() => {

if (url.includes("success")) {

resolve(`Data from ${url}`);

} else {

reject(new Error(`Failed to fetch from ${url}`));

}

}, Math.random() * 1000 + 500); // 模拟不同延迟

});

}

simulateFetch("/api/users/success")

.then(userData => {

console.log("User data received:", userData);

return simulateFetch("/api/posts/success"); // 链式调用

})

.then(postData => {

console.log("Post data received:", postData);

})

.catch(error => {

console.error("An error occurred:", error.message);

});

// 并行执行

Promise.all([

simulateFetch("/api/users/success"),

simulateFetch("/api/posts/success")

]).then(([userData, postData]) => {

console.log("All data received (Promise.all):", userData, postData);

}).catch(error => {

console.error("Error in Promise.all:", error.message);

});

机制: fetch 等异步操作,会将真正的网络请求交给浏览器或Node.js的底层API(通常在后台线程完成)。一旦操作完成,其结果(或错误)的回调函数会被推入微任务队列。事件循环优先处理微任务,因此Promise的回调会比 setTimeout 的回调更早执行(只要Promise在事件循环检查时已经完成)。

3. async/await 语法糖

async/await 是一种更高级的语法糖,它建立在 Promise 之上,使得异步代码看起来更像同步代码,极大地提高了可读性。

实践:

<JAVASCRIPT>

async function fetchUserDataAndPosts() {

try {

console.log("Starting async/await fetch...");

const [users, posts] = await Promise.all([

simulateFetch("/api/users/success"),

simulateFetch("/api/posts/success-again")

]);

console.log("Async/await data:", users, posts);

} catch (error) {

console.error("Async/await failed:", error.message);

}

console.log("async/await function finished.");

}

fetchUserDataAndPosts();

机制:

async 函数会隐式返回一个 Promise。

await 关键字会暂停async 函数的执行,直到其后的 Promise 被 resolved 或 rejected。

当 await 暂停时,JavaScript引擎会立即去检查微任务队列(如果存在)并执行微任务。一旦微任务执行完毕,或者没有微任务,async 函数才会被恢复执行。

4. Web Workers (浏览器环境)

Web Workers 允许你在后台线程中运行JavaScript代码,完全避免了对主线程的阻塞。这是实现真正意义上的“并行”计算。

实践:

创建 Worker 线程:new Worker('worker.js')

主线程向 Worker 发送消息:worker.postMessage(data)

Worker 线程接收消息:self.onmessage = function(event) { ... }

Worker 线程处理计算,然后将结果发送回主线程:self.postMessage(result)

主线程接收 Worker 的消息:worker.onmessage = function(event) { ... }

main.js (主线程):

<JAVASCRIPT>

console.log("Main thread started.");

const myWorker = new Worker('worker.js');

myWorker.postMessage({ number: 1000000000, type: 'calculateFibonacci' }); // 发送任务

myWorker.onmessage = function(event) {

console.log("Message received from worker:", event.data);

if (event.data.type === 'fibonacciResult') {

console.log(`Fibonacci of 1000000000 is: ${event.data.result}`);

myWorker.terminate(); // 工作完成后终止 Worker

}

};

console.log("Worker message sent. Main thread continues.");

worker.js (Worker 线程):

<JAVASCRIPT>

function fibonacci(num) {

if (num <= 1) return num;

// 这是一个耗时的计算

let a = 0, b = 1;

for (let i = 2; i <= num; i++) {

let temp = a + b;

a = b;

b = temp;

}

return b;

}

self.onmessage = function(event) {

const { number, type } = event.data;

console.log(`Worker received task: ${type} with number ${number}`);

if (type === 'calculateFibonacci') {

const result = fibonacci(number);

self.postMessage({ type: 'fibonacciResult', result: result });

}

};

机制: Web Workers 通过创建独立的全局上下文和独立的线程来运行,它们之间通过消息传递 postMessage 来通信。Worker 线程中的代码执行不会阻塞主线程。

5. Node.js 的 worker_threads / child_process

在 Node.js 环境中,worker_threads 模块提供了与浏览器 Web Workers 类似的功能,允许你在独立的线程中运行JavaScript代码,实现真正的并行计算。child_process 模块则允许你创建新的进程来执行外部命令或Node.js脚本,也是一种进程级别的并行。

三、 JavaScript 并发编程的常见误区

误区一:JavaScript 是多线程的。

真相: JavaScript 的代码执行是 单线程 的。事件循环和Web API/Node.js API的后台线程只是辅助了异步操作,但JavaScript代码本身仍然是在一个主线程上顺序执行的。Web Workers 是例外,它们创建的是独立的JS线程。

误区二:“同时”等于“并行”。

真相: JavaScript 的并发(通过事件循环、Promise、async/await)是一种模拟,是异步非阻塞的执行方式,而非真正意义上的并行。它通过在等待耗时操作时切换到其他任务来提高效率。真正的并行,是通过多线程(Web Workers, Node.js worker_threads)或多进程来实现的,即CPU可以同时执行多个任务。

误区三:Promise 全部是微任务。

真相: Promise 的 .then()、.catch()、.finally() 以及 queueMicrotask 创建的任务是微任务。但 setTimeout(callback, 0) 或 setInterval 的回调是宏任务。宏任务会在一个事件循环迭代结束后,检查微任务队列并执行完毕后,才被推入调用栈执行。因此,Promise 的回调通常会比 setTimeout(0) 的回调先执行。

误区四:async/await 能够自动实现并行。

真相: await 关键字本身是顺序的。它会暂停当前 async 函数的执行,直到其后的 Promise settled。如果你想让多个异步操作并行执行,需要结合 Promise.all、Promise.race 等方法:

<JAVASCRIPT>

async function sequentialFetch() {

console.log("Starting sequential fetches...");

const data1 = await simulateFetch("/api/data1"); // 等待 data1 完成

console.log("Received data1:", data1);

const data2 = await simulateFetch("/api/data2"); // 等待 data2 完成

console.log("Received data2:", data2);

console.log("Sequential fetches done.");

}

async function parallelFetch() {

console.log("Starting parallel fetches...");

const [data1, data2] = await Promise.all([ // Promise.all 确保它们并行启动

simulateFetch("/api/data1"),

simulateFetch("/api/data2")

]);

console.log("Received data1:", data1);

console.log("Received data2:", data2);

console.log("Parallel fetches done.");

}

误区五:Web Workers 可以直接访问主线程的 DOM。

真相: Web Workers运行在独立的线程和全局上下文中,它们无法直接访问主线程 DOM。它们只能通过 postMessage 进行消息通信。所有 DOM 操作都必须在主线程上进行。

四、 实践建议

善用 async/await: 提高代码可读性,但要记得结合 Promise.all 实现并行。

理解微任务与宏任务的优先级: 在需要精确控制执行顺序时,了解它们的区别非常重要。

Web Workers 是处理 CPU密集型任务的利器: 对于复杂的计算、数据处理,考虑使用 Web Workers 来避免阻塞主线程。

Node.js 中的并发:

CPU密集型任务:考虑 worker_threads。

I/O密集型任务:Node.js的异步非阻塞模型(事件循环)已经做得很好。

子进程:child_process 模块用于执行外部命令或独立应用程序。

错误处理: 确保你的异步代码有健壮的错误处理机制(try...catch for async/await, .catch() for Promises)。

五、 结论

JavaScript 的并发编程,核心在于理解其单线程模型如何通过事件循环、Web API/Node.js API以及 Promise/async-await 等机制,有效地处理异步操作,避免阻塞。而 Web Workers 或 Node.js 的 worker_threads 则为我们提供了实现真正并行计算的途径。

通过清晰地认识 JavaScript 的并发模型,破除常见的误区,我们可以更有效地利用语言特性,编写出高性能、响应迅速的Web应用和服务器端应用。希望本文能为你对JavaScript并发编程提供更深入的理解。

http://www.xdnf.cn/news/20138.html

相关文章:

  • 软考高级 — 系统规划与管理师考试知识点精要
  • 电脑活动追踪全解析:六款软件助企业实现数字化精细管理
  • whl编译命令作用解释
  • 【完整源码+数据集+部署教程】加工操作安全手套与手部检测系统源码和数据集:改进yolo11-cls
  • mysq集群高可用架构之组复制MGR(单主复制-多主复制)
  • 2025 年 8 个最佳网站内容管理系统(CMS)
  • 小迪安全v2023学习笔记(七十八讲)—— 数据库安全RedisCouchDBH2database未授权CVE
  • LeetCode 刷题【65. 有效数字】
  • 机器学习算法介绍二
  • postgresql 通过dblink实现 跨库查询
  • PostgreSQL收集pg_stat_activity记录的shell工具pg_collect_pgsa
  • zoho crm notes add customer fields
  • 数字人打断对话的逻辑
  • 本地 Ai 离线视频去水印字幕!支持字幕、动静态水印去除!
  • python-虚拟试衣
  • LVS、Nginx与HAProxy负载均衡技术对比介绍
  • 任意齿形的齿轮和齿条相互包络工具
  • Linux常见命令总结 合集二:基本命令、目录操作命令、文件操作命令、压缩文件操作、查找命令、权限命令、其他命令
  • Process Explorer 学习笔记(第三章3.2.5):状态栏信息详解
  • PyTorch 训练显存越跑越涨:隐式保留计算图导致 OOM
  • 机器学习周报十二
  • 基于Echarts+HTML5可视化数据大屏展示-旅游智慧中心
  • CC-Link IE FB 转 DeviceNet 实现欧姆龙 PLC 与松下机器人在 SMT 生产线锡膏印刷环节的精准定位控制
  • docker 安装kafaka常用版本
  • 错误波形曲线
  • Qt信号与槽机制全面解析
  • Redis 事务:餐厅后厨的 “批量订单处理” 流程
  • 两条平面直线之间通过三次多项式曲线进行过渡的方法介绍
  • 雅菲奥朗SRE知识墙分享(七):『可观测性的定义与实践』
  • C++两个字符串的结合