文件

核心概念

RJS 類別(src/model/RJS.ts)如何解析模板指令,並以單次、非響應式的方式渲染 DOM。

實例生命週期

"#selector".RJS(body) 透過 body.id 查找目標元素,複製一份作為原始模板(#model),並將 dataeventwhen 儲存於實例上。若選擇器解析結果為 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)),將傳入的 dataeventwhen 鍵合併進現有實例狀態(未提及的鍵維持原值),並重新執行 #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 屬性的元素。#setPathfetch 目標 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 兄弟節點收集為一組,依序評估各條件,僅保留第一個符合的節點(同組內即使後方節點自身條件成立,也一律移除)。支援的運算子:><>=>==<=<=======!=!====!= 會先特判 nulltruefalseempty(空字串檢查)等字面值,其餘才回退為字串相等比較。

:model — 表單雙向綁定

#FIT_MODEL 依綁定元素型別分派:checkbox/radio 監聽 change,並將所有勾選值以 , 串接;select 監聽 change;純 inputtextarea 同時監聽 keyupchange。無論哪種情況,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 中每一段被註解掉的程式碼(#dataListenerObject.defineProperty 的 getter/setter 對)都是被移除的響應式 observer 機制。現行設計每次 #init() 只渲染一次,需要明確呼叫 renew() 才會更新——這項取捨正是 README 所述「移除自動監聽帶來的效能負擔」。

EN