文件
架構
這一頁寫給想了解 count.set(1) 到畫面上標籤產生變化之間究竟發生了甚麼事的應用程式開發者,適用於以 Rust 或 TSX 編寫的應用程式。
你不需要閱讀本頁也能開發應用程式,但它能解釋 NSH 為何會有這樣的行為:為何元件只執行一次、為何 JS 忙碌時動畫仍然流暢、為何主執行緒從不等待你的程式碼。
#整體概覽
#建置:只有你的程式碼會變成 JS
nsh build、nsh dev 及其他指令會使用以 Rust 編寫的工具鏈(不需要 Node.js)來打包你的應用程式:
- Oxc 移除 TypeScript 型別。
- NSH 的 JSX 編譯器在同一個處理階段中,把 JSX 轉換成對原生 renderer 的直接呼叫。靜態的 props 和子節點只會設定一次;可能會變化的運算式(呼叫函式或讀取屬性的運算式)則會變成 renderer 追蹤的綁定。元件會變成一般的函式呼叫,並接收一個 props 物件,其中會變化的 props 以 getter 提供。
- rolldown 把你的模組打包成單一 ES module。對 NSH 模組(
nsh、nsh/mobile、nsh/base等)的 import 會保持為 import:這些模組是原生模組,由 runtime 提供。打包結果只包含你應用程式的程式碼;計數器範例的 JS 不到 1 KB。 - 打包結果會編譯成位元組碼(
bundle.nshc),載入速度比原始碼更快;同時會跨所有步驟合成一份 source map,讓堆疊追蹤指向你的.tsx行號。
你匯入的圖片和字型會被收集為資源,而供編輯器使用的型別宣告則會寫入 .nsh/types。
以 Rust 編寫的應用程式沒有打包結果:Cargo 會為每個目標建置 runner 並連結你的 crate,而 nsh.json 所指定的根元件會在引擎上以原生方式執行。以下所述的一切同樣適用於 Rust 應用程式,只是你的元件程式碼在 TSX 應用程式中於 QuickJS 內執行,在 Rust 應用程式中則以 Rust 執行。
#執行:每個應用程式兩個執行緒
每個正在執行的應用程式(iOS 上的 NshAppView、Android 上的 NshView,或已打包應用程式的視窗)都是一個引擎,擁有自己的引擎執行緒;若是 TSX,還會有自己的 QuickJS VM。這裡沒有全域單例,因此多個應用程式可以在同一個宿主應用程式中並行運作。
#引擎執行緒
應用程式邏輯的一切都在這裡進行:
- 你的程式碼在這裡執行:元件、事件處理器、計時器、effect。Rust 元件以原生方式執行;TSX 則在 QuickJS 中執行。
- 響應式圖(signal、computed 值、effect、owner、context)是原生的。它的節點是原生類別的 QuickJS 物件,因此垃圾回收器能透過它們看見引用關係,但依賴追蹤和更新排程都由 Rust 負責(遵循 Solid 1.9 的演算法)。
- 你編譯後的 JSX 所呼叫的 renderer 是原生的。它建立節點,並把每個變更轉換成一個直接推入原生佇列的 op:沒有 JSON,也不會每個 prop 傳送一次訊息。
- 核心維護所有節點的 shadow tree。當一個 JS turn 結束時,它會一次取出所有已排入佇列的 op 並進行 commit:渲染 Rust 元件(
nsh/base的元件及應用程式自己的元件)、解析 padding 和 margin 中的env()inset、重建有變化的文字、以 Taffy 進行佈局(支援 flexbox 和 grid,預設為縱向排列且不收縮)、比對 frame 和樣式的差異,並產生一個 MountBatch:即原生視圖所需的操作清單。
#主執行緒
平台的主(UI)執行緒會以單次跳轉套用每個 MountBatch:建立視圖、插入視圖、設定樣式和 frame,並交出已完成佈局的文字。除此以外,它只做必須在主執行緒上進行的工作:
- 繪製文字,而這些文字已在引擎執行緒上完成佈局;
- 原生動畫:來自
nsh的動畫值會逐幀驅動(iOS 上使用CADisplayLink,Android 上使用 UI 執行緒的 animator),並直接寫入視圖; - 手勢和捲動:觸控會變成事件並傳送到引擎執行緒;
scrollValue和pan會在同一幀內為動畫值提供輸入。
主執行緒從不等待 JS。 當系統需要即時答覆時(例如這段文字可否更改、返回按鈕可否彈出此畫面、此 sheet 可否關閉),答案已預先以 prop 的形式掛載,而 JS 之後會透過事件得知結果。引擎執行緒繁忙可能會延遲更新,但不會令捲動、動畫或系統手勢卡住。
#從點按到畫面
main thread engine thread main thread
─────────── ───────────────────────────────────────────────────── ───────────
tap on a view ─ event ─▶ handler runs: count.set(count() + 1)
reactive graph: the bindings that read count() run again
renderer: one op per changed binding → native queue
JS turn ends → core commit: render, text, layout, diff
→ MountBatch (e.g. 1 Text item) ─ hop ─▶ apply: draw the new text
在計數器範例中,一次點按在 Mac 的 release 建置上,JS 和 commit 合計約需 0.12 毫秒,並產生少量的 mount 項目(參閱測試)。
在 Rust 應用程式中,處理器是一個 Rust closure(count.set(count.get() + 1)),之後同樣是相同的響應式圖、commit 和 batch,過程中沒有直譯器。
這個設計帶來三項特性:
- 一個 turn 就是一次 commit。 不論處理器設定了多少個 signal,視圖都只會一併變更一次。
- 元件只執行一次。 只有綁定會重新執行;元件不會重新渲染,也沒有虛擬 DOM 比對。剩下的比對工作屬於核心,並以 Rust 對 frame 和樣式進行。
- 只傳送變更。 沒有變化的樣式和 frame 不會被傳送;新建立的視圖也不會獲得預設值。
#文字:只佈局一次,且不在主執行緒進行
文字往往是 UI 中成本最高的部分。NSH 在引擎執行緒上使用平台自己的文字引擎來佈局文字,所以外觀與原生文字完全一致:
- 準備:一個
<Text>及其巢狀的 span 會合併成一個帶屬性的字串(Apple 平台使用 CoreText,Android 使用PrecomputedText/StaticLayout); - 在佈局所要求的寬度下進行量度;量度結果會按內容和寬度快取,因此只更改顏色時不會重新量度;
- 以最終尺寸進行佈局。
主執行緒接收不可變的結果,並只負責繪製。巢狀在 <Text> 內的文字是 span,而不是視圖;位於 <Text> 以外的字串不會渲染任何內容。
#列表
List 以原生方式進行虛擬化。核心會決定哪些列靠近可視範圍、計算位置、欄數和 sticky header,並回收儲存格。當儲存格需要顯示不同的項目時,runtime 會為它綁定資料(你的列函式的 item() 和 index() accessor 會隨之改變);如果某一幀沒有顯示新的列,則完全不會呼叫 JS。參閱列表。
#runtime 的其他工作
- 網絡:
fetch及其他全域 API 都是原生類別。請求會交給平台的 HTTP 堆疊處理(NSURLSession、HttpURLConnection),並支援雙向串流。 - 圖片會在主執行緒以外下載和解碼,按視圖的大小處理,並設有記憶體快取。
- 限制和權限由 runtime 以 Rust 強制執行:參閱沙盒與權限。
- 熱模組替換同樣是原生的:runtime 套用修補只需遠少於一毫秒(參閱開發伺服器與除錯)。
#三個層次
NSH 的 UI 與 GPUI Kit 一樣,分為三個層次構建:
| 層次 | NSH | GPUI Kit | 說明 |
|---|---|---|---|
| 基礎元素 | nsh-core 與 runtime(nsh、nsh/mobile、nsh/desktop) |
gpui |
宿主元素、樣式、佈局、渲染、響應式機制、環境 |
| 基礎元件 | nsh-base(nsh/base) |
gpui-base |
無樣式的元件,負責行為、狀態、焦點、鍵盤和無障礙功能,外觀則由應用程式決定 |
| 元件庫 | 你的應用程式自己的,或社群提供的 | gpui-component |
有樣式的元件:按鈕、卡片、帶有外觀的列 |
前兩個層次屬於 Native Shell,第三個層次則由你負責:應用程式可以在 nsh/base 和基礎元素之上建立自己的元件庫(以 TSX 或作為自己 crate 中的 Rust 元件),也可以使用社群發佈的元件庫,或直接在應用程式中為 UI 設定樣式。
#Crate 一覽
NSH 是一個 Rust workspace。開發應用程式時你無需自行建置它(發行版已附帶 CLI 和預先建置的 runtime),但這些名稱會出現在日誌和文件中:
| Crate | 說明 |
|---|---|
nsh-core |
與平台無關的核心:shadow tree、樣式、Taffy 佈局、文字合併、列表虛擬化、mount 項目;CSS 值和 Tailwind;原生動畫圖(數值、插值、timing / spring / decay);Rust 元件的框架和主題;runtime 的 Rust 模組(nsh/mobile 的 BackHandler 等)。 |
nsh-base |
nsh/base,即無樣式的基礎元件(NSH 的 gpui-base):導覽、對話框、控制項和狀態列;Rust 應用程式以 nsh_base 使用它。 |
nsh-js |
QuickJS 宿主和 NSH 的 runtime:響應式圖、renderer、所有原生模組、全域 API、熱模組替換、限制;以及以上所有內容的 TypeScript 宣告。 |
nsh-engine |
引擎執行緒:驅動 nsh-js 和 nsh-core,並每幀向平台交出一個 batch。 |
nsh-package |
已簽署的套件:金鑰、憑證、撤銷、信任存放區。 |
nsh-bundler |
打包器:rolldown、Oxc、NSH 的 JSX 編譯器、source map、Tailwind class 檢查。 |
nsh-dev |
開發伺服器及其客戶端。 |
nsh-platform-ios |
UIKit 後端(iOS、模擬器、Mac Catalyst):視圖掛載、CoreText 文字引擎、輸入;嵌入式 SDK。 |
nsh-platform-android |
Android 後端:libnsh.so 和 Java runtime(NshView、視圖、圖片、網絡)。 |
nsh-cli |
nsh 指令。 |
依賴方向為 nsh-core ← nsh-js ← nsh-engine ← 各平台;nsh-base 建基於 nsh-core。核心不依賴任何平台,也不依賴任何 JS 引擎,因此 nsh test 能在任何機器上以 headless 方式執行真正的 runtime。