JavaScript的设计原点
你有没有想过,当你在浏览器里打开一个网页,看到的一切,本质上都是文件。
- 文字是
.html文件 - 样式是
.css文件 - 逻辑是
.js文件 - 图片是
.jpg/.png文件
浏览器要做的,就是在几秒内,同时下载几十上百个这样的文件,然后按照 HTML 的指令,把它们渲染成你看到的页面。
这个场景,在计算机领域有一个专门的名字:高并发 I/O(High Concurrency I/O)。
我们在操作系统课上学过这样的传统的多线程模型,要同时下载这么多文件,通常会这样做:
为每个文件创建一个线程,让它们"同时"去下载。
但这里会遇到三个现实问题:
- 线程开销:下载 100 个小文件,就要创建 100 个线程。按照Windows默认的栈空间1M计算,开一个页面,仅线程的栈内存开销就要100MB!
- 多个线程同时访问网络缓冲区、磁盘缓存、DOM 结构时,必须加锁。锁会带来阻塞、死锁、性能下降。
简单计算我们可以发现:
传统多线程模型,是为"少量但长期运行的任务"设计的,不适合"大量但短暂的 I/O 任务"。
为了满足现代浏览器在高并发 I/O 场景下的高性能要求,需要一种全新的并发模型。
JavaScript 没有选择多线程,采用了单线程的解决方案:
“JavaScript defines the concept of an agent. This section gives the mapping of that language-level concept on to the web platform. … Such code can involve multiple globals/realms that can synchronously access each other, and thus needs to run in a single execution thread.”
这类代码可能涉及多个可以同步访问彼此的全局对象/领域,因此需要在单个执行线程中运行。
为什么单线程可以解决这样的并发难题?
- 所有 I/O 操作(下载文件、读取磁盘、等待网络)都是非阻塞的
- 当一个文件在下载时,主线程不会盲等,而是立刻去发起下一个文件的下载。(异步)
这是 JavaScript 单线程 + 非阻塞 I/O 模型的设计原点,也是 Node.js 敢于用 JS 做后端的底气所在——通过异步的方式解决并发问题。
Promise——单线程下管理异步操作的解决方案
1 | // 问题:单线程下,如果网络请求等3秒,线程就不能做任何事 |
在事件驱动的异步编程模型中,程序的执行流程由“事件”本身驱动,而具体响应动作的逻辑则通过“事件处理程序”(或称为“事件监听器”)来定义。这两个概念共同构成了该模型的基础机制。
Promise 如何解决这个矛盾?
=>单线程特性:一次只做一件事,不能"等待"
=>需要处理耗时操作(网络、定时器、用户点击)
=>如何不阻塞线程又能拿到结果?
Promise 设计模式:
- 立即返回"承诺对象"(占位符)
- 异步操作完成后,通过事件循环通知
- 注册的回调在合适的时机执行
=>完美适配单线程的事件循环机制
为解决阻塞问题,Promise 被立即返回一个“承诺对象”(图中用虚线边框票据表示),作为异步任务的占位符。它在创建时刻(t0+ε)就已返回,不阻塞主线程。
事件循环与回调队列
异步操作由 Web APIs 在后台完成,完成后将回调放入回调队列。事件循环不是被动推送,而是在每个 tick 中主动轮询:
- ① 检查调用栈是否为空
- ② 若为空,从队列前端拉取一个回调
- ③ 将其推入调用栈执行
点击展开代码示例
1 | // ✅ Promise 提供的解决方案 |