网安创新实践2️⃣:JavaScript语言基础

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
2
3
4
5
6
7
8
9
10
11
12
13
// 问题:单线程下,如果网络请求等3秒,线程就不能做任何事
function fetchData() {
// ❌ 如果JS能"等待",会这样写(但单线程不允许)
const data = waitForNetwork(3000); // 线程卡住3秒,页面点不动
return data;
}

// 实际单线程:不能等,函数立即返回,但结果还没到
function fetchData() {
// 发起请求后立即继续执行,3秒后结果才到
sendNetworkRequest(); // 不阻塞
// 问题:如何拿到3秒后的结果?普通return不行(函数已结束)
}

所以程序的执行顺序会被事件驱动,同时也需要事件处理程序

在事件驱动的异步编程模型中,程序的执行流程由“事件”本身驱动,而具体响应动作的逻辑则通过“事件处理程序”(或称为“事件监听器”)来定义。这两个概念共同构成了该模型的基础机制。

Promise 如何解决这个矛盾?

=>单线程特性:一次只做一件事,不能"等待"

=>需要处理耗时操作(网络、定时器、用户点击)

=>如何不阻塞线程又能拿到结果?

Promise 设计模式:

  1. 立即返回"承诺对象"(占位符)
  2. 异步操作完成后,通过事件循环通知
  3. 注册的回调在合适的时机执行
    =>完美适配单线程的事件循环机制
Promise与事件循环机制示意图

为解决阻塞问题,Promise 被立即返回一个“承诺对象”(图中用虚线边框票据表示),作为异步任务的占位符。它在创建时刻(t0+ε)就已返回,不阻塞主线程

事件循环与回调队列
异步操作由 Web APIs 在后台完成,完成后将回调放入回调队列。事件循环不是被动推送,而是在每个 tick 中主动轮询

  • ① 检查调用栈是否为空
  • ② 若为空,从队列前端拉取一个回调
  • ③ 将其推入调用栈执行
点击展开代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ Promise 提供的解决方案
function fetchData() {
return new Promise((resolve) => { // 1. 立即返回一个Promise对象(状态:pending)
setTimeout(() => { // 2. 启动一个定时器,并注册一个回调函数
resolve("数据来了"); // 4. 3秒后,定时器到期,回调函数被执行,调用resolve()
}, 3000); // resolve()将Promise的状态变为fulfilled
}); // 并传递结果“数据来了”
} // 3. fetchData()函数执行完毕,代码继续往下执行

const promise = fetchData();
console.log(promise);

promise.then(data => {
console.log(data);
});

console.log("继续做其他事");

Async与Await——更简洁的写法

https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Async_JS/Promises#async_和_await