← 博客

倒置架构

一次 React 提交如何转化为 DevTools 中的一次 console.log 调用。

Adam

在 DOM 框架中,组件转为 DOM 节点,由浏览器在页面中布局和绘制。在 Text.js 中,组件转为单元格,每个发生变化的单元格帧转为 DevTools 中的一次 console.log 调用。我们称之为倒置架构。

处理流水线

React ─▶ reconciler ─▶ Yoga ─▶ CellBuffer ─▶ row-hash gate ─▶ RLE ─▶ console.log

每个阶段的存在,都是因为控制台是一种特殊的显示界面。它使用等宽字体、按行组织内容,通过 %c 设置样式,并由 DevTools 决定何时重绘。

协调器

@textjs/react 是一个 React 19 协调器([email protected]),提供 tx-box 和 tx-text 两种宿主元素,以及用于字符串子节点的文本实例。你导入的所有组件,从 <Box> 到 <Markdown>,都基于这些元素和实例构建。提交通过 requestAnimationFrame 调度,按键或粘贴操作会在同一任务中同步渲染。

布局

编译为 WebAssembly 的 Yoga 使用 pointScaleFactor = 1 为树布局,让每个盒子都对齐到完整的单元格。文本通过 Intl.Segmenter 和东亚字符宽度表测量,因此 你好 占 4 个单元格,🚀 占 2 个。如果一次编辑不会改变文本节点的尺寸,就不会再次调用 Yoga。

CellBuffer

帧是由一次性分配的类型化数组构成的网格:字符存储在 Uint32Array 中,前景色和背景色打包为 0xRRGGBBAA,属性存储为 Uint16Array 位掩码。绘制时写入这些数组,并记录涉及的行。

哈希关卡

每行都缓存一个 32 位 FNV-1a 哈希值。只有自上一帧以来写入过的行才会重新计算哈希。如果没有行发生变化,驱动会在构建字符串之前返回,不调用控制台。隐藏标签页中的加载转圈图标不产生开销,因为它不会推进动画。

游程编码

发生变化的行会被重新扫描,划分为样式相同的游程,每个游程转为一个 %c span。游程可以跨越换行。CSS 字符串通过整数样式 ID 驻留复用,因此热路径绝不会重复构建同一个字符串。一个 100×32 的仪表盘包含 3200 个单元格和 149 个 span。

格式字符串还会防范 DevTools 的两种行为。字面量 % 写作 %%。在 :// 后面、www. 内部和 path:line 中插入 word joiner,避免 DevTools 将文本转为链接,再用自己的颜色重绘。

一次调用

每个发生变化的帧只需一次调用:

console.log("%c\u200b%c\n%c▼ OpenCode Lite%c …", "margin-top:100vh;", "", "color:#fafafa;…", …);

帧以追加方式输出,节奏与 DevTools 的 50 ms 节流器对齐,并在一次绘制内完成切换。Turbolog:消除闪烁介绍了这部分。

输入

控制台无法接收按键,因此由页面接收。隐藏的 <textarea> 收集按键、粘贴和输入法组合输入,wheel 事件则滚动 ScrollBox。在 textjs.org 上,Playground 和 OpenCode 页面各自都是一整块焦点区。打开控制台,然后点击页面即可输入。

页面能看到什么

页面无法读取控制台的尺寸,只能读取自身窗口的尺寸,因此 Text.js 会推算控制台尺寸:停靠面板会缩小 innerWidth 或 innerHeight,而 outer* 保持不变,再针对 Chrome 的缩放级别求解每一种停靠位置假设。选择最匹配的结果。这些常量(56 px 的水平内边距、104 px 的控制台界面区域、14 px 的行距)均在真实的 DevTools 前端中测得。