核心概念
RJS 類別(src/model/RJS.ts)如何解析模板指令,並以單次、非響應式的方式渲染 DOM。
實例生命週期
"#selector".RJS(body) 透過 body.id 查找目標元素,複製一份作為原始模板(#model),並將 data/event/when 儲存於實例上。若選擇器解析結果為 null,建構子會提早返回,不會初始化任何實例。
graph TB
Entry["#selector.RJS(body)"] --> Ctor["建構子: 查找節點, cloneNode(true)"]
Ctor --> Init["#init()"]
Init --> Before["when.before_render()"]
Before -->|"回傳 false"| Cancel["渲染中止"]
Before -->|"繼續"| SetDOM["#SET_DOM(body, data) — 遞迴走訪"]
SetDOM --> After["when.rendered()"]
app.renew(body) 會從複製的 #model 還原 DOM(replaceChildren(...this.#model.children)),將傳入的 data/event/when 鍵合併進現有實例狀態(未提及的鍵維持原值),並重新執行 #init()。這是 RJS 實例重新渲染的唯一方式——沒有自動響應式或髒值檢查機制。
指令解析順序
#SET_DOM 依序走訪每個子元素並套用指令。帶有 :path 或 :for 標籤的節點會短路進入各自的處理函式,並跳過該節點後續的流程:
graph TB
Node["子元素"] --> PathCheck{"有 :path 屬性?"}
PathCheck -->|是| SetPath["#setPath: 抓取片段並內嵌,返回"]
PathCheck -->|否| ForCheck{"有 :for 屬性?"}
ForCheck -->|是| FitFor["#FIT_FOR: 展開迴圈,返回"]
ForCheck -->|否| SetIf["#setIf: :if / :else-if / :else"]
SetIf --> FitModel["#FIT_MODEL: 表單雙向綁定"]
FitModel --> FitAttr["#FIT_ATTRIBUTE: :[attr] / :[css]"]
FitAttr --> FitEvent["#FIT_EVENT: @[event]"]
FitEvent --> FitText["#FIT_TEXT: {{ }} 文字插值"]
FitText --> Recurse["#SET_DOM(child) — 遞迴至孫節點"]
尚未由 #setIf 判定完成(後方仍有待解析的 :else-if/:el-if/:else 兄弟節點)的元素會在這一輪被跳過;#setIf 移除落敗分支後,會對父節點重新呼叫一次 #SET_DOM。
:path — 非同步片段載入
僅套用於帶 :path 屬性的元素。#setPath 會 fetch 目標 URL,將回傳的 HTML 作為子節點插入,並對該片段重新執行 #SET_DOM(除非片段內含 new PD 建構式,此時不處理)。片段中的 script 標籤會被移至 document.body 底部執行;其餘內容原地展開,原本的佔位節點會被移除。fetch 失敗時同樣會移除該節點。
:for — 迴圈展開
支援三種格式:item in items、(item, index) in items、(key, value) in object。#FIT_FOR 針對每個陣列項目(或物件鍵)複製一份模板節點,綁定 subItem = { [val0]: item, [val1]: index }(或 { [val0]: key, [val1]: value }),對每份複本渲染屬性/事件/文字後,將整批節點插回原節點所在位置。巢狀物件項目會進行完整的遞迴 #SET_DOM;純值項目則只做屬性/事件/文字代換。
:if / :else-if / :el-if / :else
#setIf 會將 :if 節點與其後緊接的 :else-if/:el-if/:else 兄弟節點收集為一組,依序評估各條件,僅保留第一個符合的節點(同組內即使後方節點自身條件成立,也一律移除)。支援的運算子:>、<、>=/>==、<=/<==、==/===、!=/!==。==/!= 會先特判 null、true、false、empty(空字串檢查)等字面值,其餘才回退為字串相等比較。
:model — 表單雙向綁定
#FIT_MODEL 依綁定元素型別分派:checkbox/radio 監聽 change,並將所有勾選值以 , 串接;select 監聽 change;純 input/textarea 同時監聽 keyup 與 change。無論哪種情況,handler 都是直接寫入該次渲染傳入的 data 物件——寫入本身不會觸發任何 observer 或響應式重新渲染。
:[attr] / :[css] — 屬性與樣式綁定
#FIT_ATTRIBUTE 收集所有符合 /^:[\w\-]+$/(排除上述指令名稱)的屬性,透過 #GET_VALUE 解析其值後交給 #SET_ATTRIBUTE。:class 會呼叫 _class()(見 API 參考);:id/:src/:alt/:href/:html 直接對應到相同名稱的 DOM 屬性;其餘 camel-case 後存在於 element.style 上的鍵視為 CSS 屬性;其餘一律作為純 HTML attribute(若解析值為空則移除)。
@[event] — 事件綁定
#FIT_EVENT 比對 /^@[\w]+$/ 格式的屬性,將 @click 改寫為 onclick,並在 this.event 中查找對應的 handler 名稱。在 :for 迴圈內,handler 名稱本身也可以是逐項資料值(透過與 {{ }} 插值相同的 key-path 解析),而非固定方法名。
{{ }} — 文字插值
#FIT_TEXT(頂層)/#FIT_TEXT_IN_ARRAY/#FIT_TEXT_IN_OBJECT(:for 內部)會掃描文字節點中的 {{ ... }},並透過 #GET_VALUE 解析每個符合項,支援:
| 輔助函式 | 行為 |
|---|---|
{{ key.path }} |
逐層 dot-path 查找 data,透過 #GET_VALUE_FROM_LAYERS 走訪 |
{{ CALC(key + n) }} |
算術運算:+ - * / %,透過 #CALC 執行 |
{{ LENGTH(key) }} |
純物件為 Object.keys(value).length,其餘為 value.length |
{{ UPPER(key) }} / {{ LOWER(key) }} |
.toUpperCase() / .toLowerCase() |
{{ DATE(key, format) }} |
將 key 解析為 Unix timestamp 並格式化(見 API 參考 的 Number.$format) |
REGEX_TEXT 也支援值嵌入周圍字面文字中的情況(prefix {{ key }} suffix),代換時會保留前後綴。
非響應式是刻意設計
RJS.ts 中每一段被註解掉的程式碼(#dataListener、Object.defineProperty 的 getter/setter 對)都是被移除的響應式 observer 機制。現行設計每次 #init() 只渲染一次,需要明確呼叫 renew() 才會更新——這項取捨正是 README 所述「移除自動監聽帶來的效能負擔」。