← 部落格

反轉架構

React commit 如何轉為 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 排程,按鍵或貼上操作則在同一個任務中同步渲染。

版面配置

Yoga 編譯為 WebAssembly,以 pointScaleFactor = 1 配置樹狀結構的版面,讓每個方框都對齊整數儲存格。文字透過 Intl.Segmenter 和東亞字元寬度表測量,因此 你好 佔四個儲存格,🚀 佔兩個。如果編輯不會改變文字節點的大小,就不會再次呼叫 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> 收集按鍵、貼上與 IME 組字輸入,wheel 事件則用來捲動 ScrollBox。在 textjs.org 上,Playground 和 OpenCode 頁面各是一個大型焦點輸入區。開啟主控台,然後點擊頁面即可輸入。

頁面看見的範圍

頁面無法讀取主控台的尺寸,只能讀取自身視窗的尺寸,因此 Text.js 會推算:停駐的面板會縮小 innerWidth 或 innerHeight,而 outer* 維持不變,接著針對 Chrome 的縮放層級求解每種停駐位置的假設。最吻合的結果勝出。這些常數(56 px 的水平內距、104 px 的主控台介面、14 px 的列距)都是在實際的 DevTools 前端中測量的。