从调用栈到 Vite:现代 JavaScript 代码究竟如何运行

学习 JavaScript 时,最容易混淆的是语言、运行环境和开发工具。JavaScript 规定变量、函数、Promise 和模块等语法;浏览器与 Node.js 提供真正的运行环境;Vite 则负责转换和组织代码。

调用栈与事件循环

JavaScript 会先在调用栈中执行同步代码:

console.log("A");

setTimeout(() => console.log("T"), 0);

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

console.log("B");

输出顺序是:

A
B
P
T

JavaScript 首先执行所有同步代码,因此先输出 AB。Promise 回调进入微任务队列,定时器回调进入任务队列。当前同步代码结束后,JavaScript 会先清空微任务,再执行下一个任务。

可以先记住这条通用规则:

同步代码
-> Promise.then / queueMicrotask
-> setTimeout / 用户事件 / 网络回调

setTimeout(fn, 0) 并不表示立即执行,只表示达到最短等待时间后,把回调安排到后续任务中。

async/await 也建立在 Promise 之上。执行到 await 时,当前函数会暂停,后续部分进入微任务,但整个 JavaScript 线程不会被阻塞。

浏览器与 Node.js

JavaScript 语言本身不包含 DOM 或文件系统。它们由运行环境提供:

浏览器:document、DOM、localStorage
Node.js:process、node:fs、服务器能力

因此,document.querySelector() 只能在浏览器环境中直接使用,而 node:fs 属于 Node.js。较新的 Node.js 也提供 fetch,但仍然没有浏览器 DOM。

ES Module 如何工作

现代 JavaScript 使用 importexport 组织模块:

// math.js
export function add(a, b) {
  return a + b;
}
// main.js
import { add } from "./math.js";

console.log(add(2, 3));

模块加载大致分为解析、链接和执行三个阶段。运行环境先找出模块之间的依赖关系,再按照依赖顺序执行代码。

同一个模块通常只初始化一次。后续文件再次导入时,会复用模块注册表中已有的实例。

模块导出的变量还是实时绑定,而不是简单复制:

export let count = 0;

export function increment() {
  count += 1;
}

其他模块导入 count 后,可以看到它随原模块变化,但不能直接给导入的变量重新赋值。

Vite 做了什么

浏览器不能直接理解 TypeScript 和 JSX,也通常不知道 "react" 这样的包名对应哪个文件。Vite负责:

  • 解析 npm 依赖
  • 转换 TypeScript 和 JSX
  • 提供开发服务器
  • 支持热更新
  • 为生产环境打包和优化资源

最终职责可以概括为:

Vite:转换和提供代码
浏览器:执行 JavaScript
React:计算并更新界面

理解这条执行链后,Promise 顺序、模块加载和前端构建就不再是互不相关的知识点,而是同一个 JavaScript 运行体系的不同层次。

没有回应

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注