javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript运行机制、v8原理、js事件循环

JavaScript运行机制、v8原理、js事件循环过程

作者:Hello--_--World

了解JavaScript引擎与运行机制,包括解释型与编译型语言的区别,JIT技术如何提升性能,以及JavaScript的单线程特性与异步任务处理机制,通过解析、解释、监控与优化等步骤,现代JavaScript引擎如如V8,实现了高效的代码执行

一、有了解过JavaScript引擎吗?

JavaScript运行机制有没有详细了解过?请详细说明

1. JavaScript是解释型语言还是编译型语言?

1.1 编译型 vs. 解释型:核心差异

我们可以把编程语言想象成一份外文食谱,为了让计算机(只会二进制)读懂,我们需要不同的翻译方式:

编译型语言 (Compiled)

特点

解释型语言 (Interpreted)

特点

1.2 JavaScript 属于哪一种?

结论:现代 JavaScript 是一种采用 JIT (Just-In-Time) 即时编译技术的动态语言。

虽然传统上 JS 被视为“解释型脚本语言”,但现代 JS 引擎(如 Chrome 的 V8)为了极致的性能,早已进化为混合模式。它不再是单纯地逐行翻译,而是通过 JIT 技术在运行过程中动态优化代码。

1.3 什么是 JIT(即时编译)?

JIT 结合了编译型和解释型的优点,旨在解决“解释器太慢”和“编译器启动久”的痛点。

JIT 的工作流程:

  1. 快速响应:代码加载时,解释器首先介入,快速开始执行,让用户感知不到延迟。
  2. 热点探测:在运行过程中,引擎会监控哪些代码块被频繁执行(称为 Hot Spot / 热点代码)。
  3. 即时编译JIT 编译器将这些热点代码直接编译为高效的机器码
  4. 替换执行:当再次遇到相同代码时,直接调用编译好的机器码,跳过解释步骤。

性能 ≈ 解释器的响应速度 + 编译器的执行效率 性能 \approx 解释器的响应速度 + 编译器的执行效率 性能解释器的响应速度+编译器的执行效率

1.4 核心特性对比表

特性编译型 (Compiled)解释型 (Interpreted)JIT (现代 JS / JVM)
翻译时机程序运行前运行时 (逐行翻译)运行时 (按需编译)
执行速度极快较慢接近原生速度
启动速度慢 (需等待编译完成)
跨平台性较低 (需重新编译)极高
典型代表C++, Rust, GoPython, PHPJavaScript (V8), Java

进阶知识:

现代 JS 引擎甚至拥有 “去优化 (Deoptimization)” 机制。如果 JIT 编译器根据之前的运行数据做出了错误的优化假设(例如本以为某个变量总是数字,结果突然变成了字符串),引擎会立即丢弃已优化的机器码,回退到解释器模式,以确保程序的正确性。

2. 深度解析:JavaScript 引擎 (JS Engine)

2.1 什么是 JavaScript 引擎?

JavaScript 引擎是一个专门负责解析、解释并执行 JavaScript 代码的程序。

如果把浏览器比作一辆汽车,那么 JavaScript 引擎就是这辆车的发动机。它的核心任务是:将人类可读的高级代码(JS)转换为计算机 CPU 能够理解并运行的二进制机器指令。

2.2 主流引擎概览

不同的环境和浏览器使用不同的引擎,但它们都遵循 ECMAScript 标准:

引擎名称开发者主要应用环境
V8GoogleChrome, Node.js, Electron, Edge
SpiderMonkeyMozillaFirefox
JavaScriptCoreAppleSafari, iOS 全线应用
ChakraMicrosoft早期 Edge, IE (已逐步退出舞台)

2.3 引擎内部是如何工作的? (以 V8 为例)

现代引擎的工作流程并不是简单的“翻译”,而是一个复杂的流水线:

解析 (Parsing)

解释 (Interpretation)

编译与优化 (JIT Compilation)

垃圾回收 (Garbage Collection)

2.4 为什么要理解引擎原理?

核心要点:

JavaScript 引擎并不是独立的,它运行在宿主环境(如浏览器或 Node.js)中。引擎只负责执行 JS,而 DOM 操作、网络请求(AJAX)、定时器(setTimeout)是由宿主环境提供的 Web APIs 或内置模块处理的。

3. 深度解析:浏览器引擎 (Browser Engine / Rendering Engine)

3.1 什么是浏览器引擎?

如果说 JS 引擎 是汽车的“发动机”,那么 浏览器引擎(也称渲染引擎)就是整台车的“底盘与组装车间”。

它的核心职责是:读取 HTML、CSS 和图像资源,经过一系列复杂的计算,最终将网页内容像素化并绘制在用户的屏幕上。

3.2 三足鼎立:主流引擎分布

目前市面上绝大多数浏览器都基于以下三大引擎构建:

引擎名称主要开发者代表浏览器特点
BlinkGoogle / 社区Chrome, Edge, OperaWebKit 的分支,目前生态位最强,性能优异。
WebKitAppleSafari, 所有 iOS 浏览器注重能效比,是苹果生态系统的唯一准入引擎。
GeckoMozillaFirefox坚持独立开发,高度尊重隐私与 Web 标准。

3.3 渲染流水线 (Rendering Pipeline)

浏览器引擎将代码转化为图像的过程被称为 关键渲染路径 (Critical Rendering Path)

解析 (Parsing)

构建渲染树 (Render Tree)

布局 (Layout / Reflow)

绘制 (Painting)

合成 (Compositing)

3.4 浏览器引擎 vs. JS 引擎 的协作

两者虽各司其职,但在运行过程中紧密配合:

3.5 开发者为何必须掌握它?

黄金公式:

浏览器 (Browser) = 浏览器引擎 (Blink/WebKit) + JS 引擎 (V8/JSC) + 网络模块 + UI 界面 + 各类 Web API。

4. 深度解析:V8 引擎如何执行 JavaScript 代码

4.1 解析阶段 (Parsing)

当 V8 接收到源码字符串后,会进行两步预处理:

词法分析 (Scanner):

词法分析是解析的第一步,它的核心目标是:“识字”并“切分”

切分单词(Tokenizing):将一连串的源码字符流拆分成一个个具有独立语义的单元,称为 Token(词法单元)。

过滤杂质:自动剔除代码中对逻辑运行无意义的内容,如空格、换行符、注释等。

初步分类与转换:将字符串转换为内部编号(ID)。对于引擎来说,处理数字 1(代表关键字 let)比处理字符串 “let” 要快得多。

词法错误检查:发现不符合词法规则的字符。例如在 JS 中写了一个非法的特殊符号,词法分析阶段就会报错。

语法分析 (Parser):

语法分析是解析的第二步,它的核心目标是:“组句”并“建树”。

构建 AST(抽象语法树):将词法分析产出的平铺的 Token 序列,根据语言的语法规则(文法)组装成一棵树状结构。这棵树展示了代码之间的层级和逻辑关系。

验证语法合法性:检查 Token 的排列顺序是否符合 JS 语法。

确定作用域与语义:在建树的过程中,解析器会初步确定变量的作用域(全局还是局部),并为后续生成字节码提供逻辑依据。

4.2 解释阶段 (Interpretation)

4.3 监控与分析 (Profiling)

4.4 优化编译 (JIT Compilation)

4.5 去优化 (Deoptimization)

总结:V8 的执行流水线

状态处理过程产物特点
初始源码读取字符串人类可读
分析ParsingAST 树逻辑结构化
启动Ignition字节码响应快、内存省
加速TurboFan机器码执行快、性能高

开发者启示

了解这个过程后,你会发现:保持变量类型的一致性(不要随意改变对象属性的类型或结构)能显著减少“去优化”的发生,让代码始终运行在 TurboFan 的“高速公路”上。

5 JS 引擎的运行机制与环境隔离

5.1 为什么“解释型”语言也需要先“扫描”代码?

虽然 JavaScript 是即时编译(JIT)语言,但它在执行前必须经过 Parser(解析器) 的全量扫描。

5.2 环境隔离:变量环境 vs. 词法环境

为了在兼容老旧 var 代码的同时,完美支持 ES6 的 let/const 块级作用域,V8 将执行上下文拆分为两个独立的存储区域:

变量环境 (Variable Environment)

词法环境 (Lexical Environment)

5.3 “物理隔离”带来的深远影响

这种设计决定了 JavaScript 在运行时的三个核心表现:

查找顺序

当访问一个变量时,引擎优先查找当前上下文的词法环境,若无,再查找变量环境,最后顺着作用域链向上寻找。这保证了块级变量优先于函数级变量。

块级作用域的实现

在执行过程中,每进入一个 {} 块,词法环境都会创建一个小型环境栈(Stack),并在退出块时将其销毁。这解决了 var 变量容易污染全局或循环体的问题。

暂时性死区 (TDZ)

由于词法环境中的变量在声明前处于“未初始化”状态,任何提前访问都会触发错误。这迫使开发者养成“先声明后使用”的良好习惯。

核心总结表

特性变量环境 (var)词法环境 (let/const)
提升行为提升声明并初始化为 undefined提升声明但不初始化
作用域单位函数 (Function Scope)块 ({}) (Block Scope)
重复声明允许禁止
访问限制自由访问(可能拿到 undefined)严格限制(TDZ 报错)

底层思考

这种“双环境”设计是现代 JS 引擎为了兼顾历史兼容性现代语言特性而做出的工程妥协,也是其性能与灵活性并存的秘密武器。

二、JavaScript 是单线程还是多线程?

请问异步任务处理机制是怎么样的?分别说明浏览器与 Node 的事件循环机制

1. 核心定性:JavaScript 到底是不是单线程?

结论:JavaScript 语言执行是单线程的,但其运行宿主环境(浏览器/Node.js)是多线程的。

2. 异步任务全家桶 (全面清单)

异步任务根据执行优先级的不同,分为 宏任务 (Macrotask)微任务 (Microtask)

微任务 (Microtask) —— 优先级最高

执行时机:当前调用栈清空后,立即执行,且必须清空整个微任务队列,才会进行下一次渲染或执行宏任务。

宏任务 (Macrotask) —— 优先级次之

执行时机:由宿主环境发起。每轮事件循环只取出一个宏任务执行。

3. 事件循环 (Event Loop) 执行模型

浏览器的执行顺序

  1. 执行同步代码(属于第一个宏任务)。
  2. 同步代码执行完,检查并清空整个微任务队列
  3. (视情况) 进行 UI 渲染
  4. 从宏任务队列中取入一个任务执行。
  5. 回到步骤 2,循环往复。

Node.js 的执行顺序 (libuv)

Node.js 10+ 后与浏览器基本一致,但其底层分为 6 个阶段循环:

  1. timers:执行 setTimeout 等回调。
  2. pending callbacks:执行某些系统操作的回调。
  3. idle, prepare:内部使用。
  4. poll (轮询):处理 I/O 回调,这是最核心阶段。
  5. check:执行 setImmediate 的回调。
  6. close callbacks:执行关闭回调(如 socket.on('close'))。

4. 高频面试避坑指南 (Killer Points)

Q1:Promise 内部是异步的吗?

坑点new Promise((resolve) => { ... }) 括号里的代码是同步执行的!只有 .then() 里面的回调才是异步微任务。

Q2:await 后面代码的执行顺序?

坑点await 这一行右边的表达式会立即执行。而 await 下方的代码会被阻塞,并存入微任务队列(相当于 .then)。

黄金总结

一个宏任务 → \rightarrow 所有微任务 → \rightarrow 渲染 → \rightarrow 下一个宏任务。

5. 异步任务调度题

// 作业题 console.log('stack [1]');
console.log('stack [1]'); 

setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);

const p = Promise.resolve();
for(let i = 0; i < 3; i++) {
    p.then(() => {
        setTimeout(() => {
            console.log('stack [4]')
            setTimeout(() => console.log("macro [5]"), 0);
            p.then(() => console.log('micro [6]'));
        }, 0);
        console.log("stack [7]");
    });
}

console.log("stack [8]"); 

5.1 第一轮:执行同步代码(第一个宏任务)

此时,代码从上到下扫一遍,同步代码直接进入 调用栈 执行,异步任务分发到各自队列。

当前状态:

5.2 第二轮:清空微任务队列(核心环节)

步代码跑完,调用栈空了,事件循环立即去清空所有的微任务。

执行 micro A:

执行 micro B:

执行 micro C:内部同步代码:

当前状态:

5.3 第三轮:开始执行宏任务

微任务清空后,事件循环取宏任务队列中的 第一个 任务出来执行。

执行第一个 stack [4]-A:

注意! 宏任务执行完,会立即检查并清空微任务队列。所以此时会先打印 micro [6],再跑下一个宏任务。

以此类推,执行完 B 和 C 组。

5.4 最终输出顺序结果

为了方便你核对,最终的打印顺序如下:

  1. stack [1]
  2. stack [8]
  3. stack [7] (循环1次)
  4. stack [7] (循环2次)
  5. stack [7] (循环3次)
  6. macro [2]
  7. macro [3]
  8. stack [4] (A组)
  9. micro [6] (A组微任务优先执行)
  10. stack [4] (B组)
  11. micro [6] (B组微任务优先执行)
  12. stack [4] (C组)
  13. micro [6] (C组微任务优先执行)
  14. macro [5] (A组嵌套)
  15. macro [5] (B组嵌套)
  16. macro [5] (C组嵌套)

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文