本节摘要:Electron 的本质是把 Chromium 的多进程浏览器与 Node.js 的事件循环缝合进同一套消息泵:两者都基于事件驱动模型,Electron 让它们共享同一条事件循环,Chromium 的事件(窗口、输入)与 Node 的事件(IO、定时器)在同一个线程上轮流得到调度。理解这个融合方式,能解释主进程"既像浏览器又像 Node"的种种行为,也能解释历史上有名的融合难题。
先把两台发动机看清楚。Chromium 是一台精密的渲染与交互发动机:多进程架构、合成器管线、沙箱体系,自带完整的事件系统。Node.js 是另一台:V8 引擎加 libuv 事件循环,把异步 IO 做成了看家本领。它们有共同点——都构建在事件驱动之上,都在单个线程上跑事件循环;也有深沟——Chromium 的消息泵用自己的一套任务序列,libuv 用另一套轮询机制,两套循环各说各话。
Electron 的缝合术在主进程里完成:让 libuv 的事件循环嵌入 Chromium 的消息泵,两者合并为一条循环。于是主进程的线程上,浏览器侧的任务(处理窗口事件、IPC 路由)与 Node 侧的任务(文件 IO 完成回调、网络响应)交替执行,互不阻塞又互相排队。这也解释了主进程代码的"混血气质":
// 主进程里,浏览器概念与 Node 概念自然混居 const { app, BrowserWindow } = require('electron'); // Electron 模块 const fs = require('node:fs/promises'); // Node 内置模块 const path = require('node:path'); app.whenReady().then(async () => { // Node 的异步文件读取 与 Electron 的窗口创建 写在一起毫无违和 const config = JSON.parse(await fs.readFile( path.join(app.getPath('userData'), 'window-state.json'), 'utf8' ).catch(() => '{}')); const win = new BrowserWindow({ width: config.width ?? 960, height: config.height ?? 640, x: config.x, y: config.y }); win.loadFile('index.html'); });
这段"记住窗口位置"的常见模式(第五章还会扩展成完整方案)里,app、BrowserWindow 来自 Chromium 世界,fs、path 来自 Node 世界,它们能在同一个函数里协作,正是共享事件循环的直接证据。
早期 Electron(Atom Shell 时代)有一个绕不开的工程难题:页面里要调用 Node API,意味着 Node 的代码必须跑在页面的 V8 上下文里;而页面随时可能导航、重载,V8 上下文随之销毁重建——Node 这边可能还持有旧上下文的引用,轻则泄漏,重则崩溃。Chromium 后来提供了解耦机制,让 Node 与页面上下文的绑定可以按需建立和解除,才把这个问题压下去。这段历史留下的教训一直影响至今:把 Node 直接暴露给页面,无论工程上还是安全上,都是昂贵的选择——这为上下文隔离与沙箱铺了路(2.4、2.5 节)。
另一个值得了解的融合点是模块系统。主进程里你可以同时 require Electron 模块、Node 内置模块、npm 安装的第三方包;渲染进程(现代默认配置下)则是纯浏览器环境,页面代码用的模块工具(打包器产物)与主进程互不相干。所以一个 Electron 项目里常有两套依赖图:主进程一份、前端一份,构建时分别处理——这是第三章构建工具要解决的核心问题之一。
融合机制带来一条铁律:主进程的那条线程,同时服务两个世界。任何一端的长时间占用,都会让另一端饿死。做个实验就能体会:
// 主进程里来一次致命的同步计算 app.whenReady().then(() => { // 假装在主进程做密码学运算:同步阻塞 3 秒 const stop = Date.now() + 3000; while (Date.now() < stop) { /* 自旋 */ } // 这 3 秒里:所有窗口的 IPC 全部排队、托盘点击无响应、菜单打不开 // 因为 Chromium 消息泵和 Node 循环都被这个 while 挂起了 });
正确姿势:重活要么丢给 Worker 线程,要么移到渲染进程的 Web Worker,要么干脆拆成独立子进程。规则一句话——主进程是调度台,不是加工车间。调度台一旦堵塞,整列火车都停在隧道里。
第一,定时器的精度与顺序。主进程里 setTimeout 由 libuv 驱动,但回调要等消息泵轮到它,页面繁忙或 IPC 洪峰时定时任务会迟到。别在主进程里用定时器做精确调度。
第二,process 对象的双面性。主进程里 process 是 Node 的(process.versions.node 可查);渲染进程沙箱下的 process 是 Electron 提供的阉割版,只保留少量字段。同名词不同物,排查问题时先问自己在哪个进程。
第三,版本捆绑关系。Electron 的每个发布版本都固定搭配某版 Chromium 与某版 Node,查看方式是主进程里的 process.versions。你的应用能用哪些 CSS、哪些 Node API,都被这份捆绑表决定,升级 Electron 等于同时升级两个底座——这也是 1.2 节强调"major 升级要走专门分支"的原因。
发动机的原理清楚了,下一节学变速箱:IPC——两个世界之间唯一的官方公路。