一个线程统治一切:关于 JavaScript 并发的迷思与真相
\n\n很久以前,在静态网站和会阻塞整个界面的“提交”按钮时代,JavaScript 快乐地生活在一个单线程中。这对它来说已经足够了:请求来了——执行,请求走了——界面冻结。但时代变了。如今,我们期望 Web 应用能即时响应:滚动不能卡顿,动画必须流畅,数据加载要无感。这时,并发就登场了。
\n\n许多开发者会混淆并发 (concurrency) 和并行 (parallelism)。在 JavaScript 中,尤其是在浏览器中,我们更多处理的是前者。这就像一个杂耍演员:他并不是同时让所有球都停留在空中(那是并行),而是通过巧妙地切换,制造出同时进行的假象。在本文中,我们将探讨在 JS 中组织并发的三个关键工具:async/await、协程(通过生成器和 Web Workers)以及经典线程。开始吧!
Async/Await:优雅的异步代码
\n\nasync/await 是 ES2017 中引入的、基于 Promise 的语法糖。它不会让代码变成多线程,但会让代码变得异步。当你写 await 时,JavaScript 会在这个函数中“休眠”,从而释放线程去处理其他任务。一旦 Promise 被解决,函数就会“醒来”并继续执行。这是处理 I/O 操作(如 API 请求、文件读取、定时器)的理想工具。
简单示例:从服务器加载数据
\n\n// 返回 Promise 的函数\nfunction fetchUser(id) {\n return new Promise((resolve) => {\n setTimeout(() => resolve({ id, name: '阿列克谢' }), 1000);\n });\n}\n\n// 异步函数\nasync function loadUserProfile(userId) {\n console.log('开始加载...');\n const user = await fetchUser(userId);\n console.log('用户已加载:', user.name);\n return user;\n}\n\nloadUserProfile(42);\nconsole.log('这段代码会立即执行,无需等待加载完成!');\n\n\n请注意:console.log('这段代码会立即执行...') 会比 console.log('用户已加载') 先执行。线程没有被阻塞——这就是并发的实际体现。
错误处理
\n\nasync function fetchWithRetry(url, retries = 3) {\n for (let i = 0; i < retries; i++) {\n try {\n const response = await fetch(url);\n if (!response.ok) throw new Error('HTTP 错误');\n return await response.json();\n } catch (error) {\n if (i === retries - 1) throw error;\n console.log(`第 ${i + 1} 次尝试失败,正在重试...`);\n }\n }\n}\n\n\n何时使用:任何涉及外部资源的操作(API、数据库、Node.js 中的文件系统)。async/await 是您编写异步代码的主要工具。
JavaScript 中的协程:神话还是现实?
\n\n“协程”这个术语来自 Go 语言,指的是一种轻量级的执行线程。JavaScript 没有原生的协程,但拥有生成器 (Generator Functions) 和 Web Workers,它们可以模拟类似的行为。生成器允许暂停和恢复函数的执行,从而让您能够控制执行流程。
\n\n生成器作为并发的基础
\n\nfunction* taskGenerator() {\n console.log('启动任务 1');\n yield '任务 1 完成';\n console.log('启动任务 2');\n yield '任务 2 完成';\n console.log('启动任务 3');\n yield '任务 3 完成';\n}\n\nconst iterator = taskGenerator();\nconsole.log(iterator.next().value); // 执行任务 1\n// 此时可以做一些其他事情\nconsole.log(iterator.next().value); // 执行任务 2\nconsol