本文目录 9 个章节
底层深入-WebGL应用逻辑运行时(JavaScript与WebAssembly)
# 底层深入-WebGL应用逻辑运行时(JavaScript与WebAssembly)
什么是 “JavaScript 运行时”?
运行时(runtime)= JS 引擎 + 事件循环 + 宿主 API 引擎执行脚本;事件循环调度任务;宿主提供 “外界能力”(网络、文件、WebGL 等)。
在 Web / 小程序里的两层结构
┌───────────── 宿主层 ──────────────┐
│ setTimeout / requestAnimationFrame│
│ wx.request / canvas / WebGL │
└────────────────────────────────────┘
┌─────────── JS 引擎层 ────────────┐
│ 解析(Parser) → 字节码(Interpreter)│
│ → JIT 编译(Hot code) → 机器码 │
│ GC / 隐式类型系统 / PromiseJobs │
└────────────────────────────────────┘
- Android 微信小游戏:引擎是 V8;
- iOS 微信小游戏:引擎是 JavaScriptCore (JSC);二者都 没 DOM、只暴露
wx.*API。 - 小游戏容器在 C++/Java 层维护事件循环,把宿主回调排进 宏任务队列,V8 / JSC 执行完一轮微任务后再取宏任务。
JS 代码到机器码的流水线(以 V8 为例)
- 解析:生成 AST。
- Ignition 把 AST 编译成 字节码 并立即解释执行。
- Profiling:热点函数被标记为 warm/hot。
- Sparkplug / TurboFan JIT 把热点字节码优化为本机指令;还能随时 反优化。
最终机器码直接跑在 ARM64 / x86-64 CPU 上;GC 维护堆对象与类型反馈。
WebAssembly 与 JavaScript 的关系
- 它们“共用同一个(物理)运行时”,因为引擎、事件循环、宿主线程都完全重合;
- 它们各自拥有“独立的语言 runtime”,因为字节码格式、内存模型和首段编译器彼此不同。
差异部分
物理上共用同一 runtime,但逻辑上独立。
| 项 | JavaScript | WebAssembly |
|---|---|---|
| 模块格式 | .js 源文本/ESM |
.wasm 二进制、.wat 文本 |
| 类型系统 | 动态、可变对象 | 静态, 固定位宽的 i32/f64 等 |
| 内存模型 | GC 堆(对象可移动) | 线性内存 WebAssembly.Memory(连续字节) |
| 首段编译器 | Ignition 解释器 / Sparkplug baseline | Liftoff baseline 编译器 |
| 互调桥 | WebAssembly.Instance.exports.fn() |
import "js" "log" 直接调 JS 函数 |
| 加载 API | <script>、eval |
WebAssembly.instantiateStreaming() |
示例:同一个 add 函数的两种形态
// JavaScript
export function add(a, b) { return a + b }
;; WebAssembly Text Format
(module
(func $add (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
加载并互调:
const { add } = await WebAssembly
.instantiateStreaming(fetch('add.wasm'))
.then(m => m.instance.exports);
console.log(add(40, 2)); // 42
编译流水线(以 V8 为例)
JavaScript WebAssembly
-------- ------------
source .js binary .wasm
│ parse │ decode
▼ ▼
Ignition bytecode Liftoff machine code ← 首帧快速启动
│ (解释执行 & 收集热度) │
├───────┐ ├───────┐
▼ │ ▼ │
TurboFan JIT ───────────┴─► TurboFan JIT ← 热点函数重编译
(优化、内联、SIMD…) (相同优化管线)
两条支流最后都汇入 同一个机器码调度器,在同一线程栈上运行。
共用的部分 = “引擎 + 事件循环 + 宿主 API”
两者 共用同一个引擎内核与事件循环:Wasm 模块只是 JS 引擎中的另一类 “代码对象”。当 Wasm 函数被调用时,执行栈可跨语言跳转。
| 层级 | 共享? | 说明 |
|---|---|---|
| 宿主事件循环 | ✅ | 浏览器/小游戏的宏任务队列只维护一份;JS 代码、Wasm 代码都要“让出栈”才能让 UI 与 I/O 事件继续跑。 |
| 引擎核心 (V8/JSC) | ✅ | V8 官方定义自己是 “JavaScript and WebAssembly engine”;同一个堆、同一套线程模型。 |
| 优化 JIT (TurboFan / DFG) | ✅ | 优化器既能接收 JS 字节码也能接收 Wasm IR,再生成本机指令。 |
Host APIs (wx.request, WebGL…) |
✅ | 这部分暴露给 JS,全靠 JS 调用把参数塞进 Wasm 内存后再供 Wasm 代码使用。 |
调用栈与内存的实际交错
JS (app.js)
└─ calls wasmAdd() ─► Wasm (add.wasm)
└─ calls console.log (回到 JS)
- “JS→Wasm” 时,引擎生成 TRAMPOLINE:把 JS 数值装箱/拆箱成
i32写进线性内存; - “Wasm→JS” 时走
import表,返回点仍在同一执行栈帧; - GC 线程若移动 JS 对象,只需更新托管句柄;Wasm 线性内存地址永远不变,因而无需移动补丁。
在微信小游戏 / WebGL 场景中的落地
逻辑线程只有一个:JS 执行、Wasm 执行、WebGL API 调用全在同一 V8/JSC 线程完成。
WebGL 本质是宿主(GameRuntime)暴露的 C++ API:
const id = gl.createShader(gl.VERTEX_SHADER); // JS 调 => C++ => GPUUnity WebGL:IL2CPP 先把 C# 编译成 Wasm;运行时模板在
Module.onRuntimeInitialized里用 JS 把渲染循环排进宿主事件循环。同源崩溃上报:
wx.onError(JS)+Module.onAbort(Wasm)监听到异常后,共用sendCrash()API 把数据发往服务器——JS 和 Wasm 层面虽然触发点不同,但最终走的是同一个 JS 发送函数。