Vue Vapor Mode:把 virtual DOM 蒸發掉的編譯策略

文字解析 · 1 支影片 · 產生於 2026-10-01 00:54

🎧 語音解析📝 筆記✍️ 練習

🎧 語音解析 12m51s · TTS 講解與作者原聲交錯;點章節可跳。
邊聽邊看逐句講稿,點任一句從那裡開始
章節(7)

1. Outline

  1. 起點 · Vapor Mode is the Future of Vue
    從「不改程式碼就變快」的承諾出發,先攤開 Vue 現行 virtual DOM 的完整迴圈與編譯期最佳化,指認出削不掉的固定成本,再用一句反問推出 Vapor Mode,最後用 SFC Playground 的編譯輸出對照把理論落成看得見的程式碼。

2. YouTuber 的思維推導

Vapor Mode is the Future of Vue

LearnVue · 4m08s · 字幕 en · vision=on (使用者指定 vision=true;且作者多次指著畫面說「let's take a look at the build code」「here we have a bunch of n number」「even looking at this code」,編譯結果的對照必須看畫面才懂。) · YouTube

預估 vs 實際耗時
階段預估實際
fetch 6s 1s
segment 38s 3m00s
shot 22s 3s
analyze 3m45s 10m37s
render 5s –

作者的推導是一條「先立承諾、再拆成本、最後還原機制」的鏈。他從一個誘因出發:不改任何一行程式碼就讓 app 更快、更省記憶體。但他很清楚這個承諾要能被相信,必須先讓聽眾知道現在的 Vue 把時間花在哪裡,所以他倒回去把 Vue 的執行路徑完整攤開——template 在 build time 編譯成 render function,render function 產生 virtual DOM tree,再用真實 DOM 指令掛上頁面;資料一變,整條路徑重走一次,新舊兩棵樹 diff 之後 patch 回真實 DOM。

接著他做了一個關鍵的鋪墊:先替 Vue 說好話,指出因為多了編譯這一步,Vue 能做 static hoisting 這類 runtime 框架做不到的最佳化。這一步表面上是誇 Vue,實際上是為了讓下一步的反面論證更有力——連做了編譯期最佳化的 virtual DOM 都還有削不掉的固定成本:要佔記憶體、資料一變就要重建一堆 v-node、要走完整棵樹找差異。成本落點確定後,他用一句反問把前提整個換掉:如果資料改變時我們早就知道 DOM 的哪幾個位置會受影響呢?

那麼 diff 就是在重算一個已知的答案,於是不只不用重跑 render function、不用比對,連那棵 virtual DOM tree 本身都可以不要。這就是 Vapor Mode。但這個結論押在「早就知道哪裡會變」這個前提上,所以他立刻把前提還原成 Vue 既有的 reactivity system:同一套追蹤機制,只要把訂閱的粒度從整個元件的 render function 縮小到單一 DOM 節點,前提就成立,不需要任何新發明。

理論齊了之後他換成證據模式,用 SFC Playground 把同一份 App.vue 在 VAPOR OFF 與 VAPOR ON 下的編譯輸出並排看:一邊是 _createElementVNode 組出來的樹,一邊是 n0、n1 這些真實節點變數加上 _renderEffect 直接寫文字,前面所有說法在這裡從示意圖變成看得見的程式碼。最後他從技術回到工程現實:怎麼落地(完全 opt-in、以元件為單位、可與現有元件混用)、好處的兌現條件(整包都是 Vapor 元件才能把 vdom runtime 從 bundle 移除)、以及還沒有答案的部分(Vapor 與非 Vapor 元件的功能對等程度、只支援 Composition API 與 script setup 之外還有什麼限制),把結論停在「值得放進雷達」而不是「現在就換」。

推理鏈:每段留給下一段的線索

每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。

  1. Vapor Mode 的承諾 → 作者把「不用 virtual DOM」當成好處的來源丟了出來,卻還沒說 virtual DOM 現在到底在做什麼、為什麼它會變成成本。要檢驗這張欠條,下一步必須先把現在的 Vue 從 template 到畫面的完整路徑攤開來看。
  2. Vue 現在怎麼運作:vdom 全迴圈 → 整條迴圈已經攤開,但「compiler-informed」的那半個名字還懸著沒解釋。作者留下的線索是一個問題:既然 Vue 多了一道把 template 編譯成 render function 的手續,這道手續除了轉換格式之外,還能拿來做什麼?
  3. 編譯期最佳化:static hoisting → 作者已經把「編譯期知道得比 runtime 多」立成事實,但他用的是防守姿態(Vue 比 React 做得多)。留給下一段的是一個他還沒問出口的問題:編譯期最佳化只削掉了模板裡靜態的那一半,剩下真的會變的那一半,還是得走一次完整的建樹與比對——這筆帳能不能也省掉?
  4. vdom 的代價,與拿掉它的念頭 → 結論已經下了,但它整個押在一個尚未兌現的前提上:「資料變的時候,我們早就知道哪些 DOM 會受影響」。作者到這裡一個字都還沒說 Vue 憑什麼知道。這個空缺就是下一段必須補上的。
  5. 做得到的原因:reactivity system → 原理到這裡齊了,但整條推理至今全部停留在白板方塊與四行偽程式碼上。作者自己把下一步指了出來:repo 是公開的、還有一個 playground——線索很具體,就是「同一個元件,把 Vapor 開關切一下,編譯出來的程式碼到底長什麼樣」。
  6. Playground 對照:關掉與打開 Vapor → 機制與證據都齊了。但這兩份輸出差異之大,本身就丟出了下一個問題:長成這樣的程式碼,要怎麼跟現有那些已經寫好的元件在同一個 app 裡共存?作者留下的線索是他這段結尾那句「即使不完全看懂也覺得合理」——技術上說得通之後,接著該問的是它怎麼被放進真實專案裡。
  7. 整合計畫、限制與結語

3. 逐段說明

Vapor Mode is the Future of Vue

1. Vapor Mode 的承諾 0:00–0:20

作者開門見山丟出一個誘人的假設:不改任何一行程式碼,就讓 app 更快、記憶體用得更少。這就是 Vue Vapor Mode——Vue 團隊正在開發中的實驗性編譯策略,靈感來自 Solid JS。關鍵在於:元件寫法完全不變,但跑起來不再使用 virtual DOM。他接著立下這支影片要回答的三個問題:它到底怎麼運作、能帶來什麼好處、目前開發到哪了。

承上 前情提要假設你已經寫過 Vue 的單檔元件、也在哪裡聽過 virtual DOM 這個名詞,但不必知道它內部怎麼運作。作者一開場就把這份背景當成不動的地基:他一句都沒解釋什麼是元件,而是直接拿「你現在寫的元件一行都不用改」當賣點——被換掉的是執行期的策略,不是你的寫法。

推理因為前情提要已經確立聽眾是「會寫 Vue、但沒讀過 runtime 原始碼」的人,作者選擇先給結果再給原理:他先丟出一個可以被檢驗的承諾(更快、更省記憶體、而且程式碼不用改),再立刻把它拆成三個問題——它到底怎麼運作、能帶來什麼好處、目前開發到哪了。這三個問題就是整支影片的骨架,也讓聽眾隨時知道現在這一段是在回答哪一題。

AI 補充有一個詞值得先咬清楚:作者說 Vapor 是一個 compilation strategy,不是新 API、不是新語法。改的是「同一份 template 被編譯成什麼樣的 JavaScript」,所以「不用改程式碼」不是行銷話術,而是這個設計方式的直接後果——你的原始碼是編譯器的輸入,輸入不變、輸出換一套,這在邏輯上是成立的。 他提到的靈感來源 Solid JS 也值得補一句:Solid 從第一天就沒有 virtual DOM,它用編譯把 JSX 直接變成建立與更新真實 DOM 的程式碼。Vapor 等於是把這套已經被驗證過的做法,搬進 Vue 既有的 SFC 寫法裡——這也是為什麼作者敢說寫法不用變。 最後要指出作者在這裡跳過的一步。他說「執行時不使用 virtual DOM,因此我們得到各種好處」,但「不用 virtual DOM 為什麼就會有好處」他一個字都還沒說。這是一張欠條,先記著,整支影片有沒有說服力,就看這張欠條兌不兌得出來。

Vapor Mode

Vapor 模式

Vue 的實驗性編譯策略:同一份元件 template 被編譯成不經 virtual DOM、直接操作真實 DOM 的程式碼。

名字裡的 vapor(蒸氣)帶著「把中間那層蒸發掉」的意思——蒸發掉的就是 virtual DOM。它最容易被誤解的地方是被當成一個「效能開關」或新的元件 API,其實它是編譯器的另一種輸出模式,你寫的 template 與 script 完全不動。它也不是要取代現在的 Vue,而是與現有模式並存的第二條編譯路徑。真正被換掉的只有一件事:更新畫面時,是重算一棵樹再比對,還是直接命中那個節點。

相關術語: virtual DOM (要拿掉的對象)、Solid JS (靈感來源)、compilation strategy (一種)

出處:第 1 段「Vapor Mode 的承諾」

virtual DOM

虛擬 DOM

用 JavaScript 物件描述一份 UI 樹,當成真實 DOM 的中介層,先在記憶體裡算好差異再套用到頁面。

它不是為了「比較快」而發明的,而是為了「比較好寫」:有了它,開發者只要描述「現在畫面該長什麼樣」,框架去負責算出「該改哪裡」。代價是每次更新都要付一筆固定開銷——建立物件、走訪、比對。常見的誤解是以為 virtual DOM 比直接操作 DOM 快;正確的說法是它比「開發者手寫但寫得很糟的 DOM 操作」快,比不上「精準知道要改哪裡的 DOM 操作」。

相關術語: Vapor Mode (被它移除)

出處:第 1 段「Vapor Mode 的承諾」

Solid JS

Solid JS(前端框架)

一個從設計之初就不使用 virtual DOM、靠編譯產生細粒度 DOM 更新的 JavaScript 框架。

Solid 的核心主張是:既然編譯器看得到你的模板,就沒必要在執行期再算一次哪裡變了。它的元件函式只執行一次,之後靠 signal 驅動的 effect 直接更新對應的 DOM 節點。Vapor 借的正是這個主張,不是它的語法——Solid 用 JSX,Vue 用 SFC template。

相關術語: Vapor Mode (啟發了)

出處:第 1 段「Vapor Mode 的承諾」

compilation strategy

編譯策略

編譯器把同一份原始碼轉換成哪一種執行期程式碼的整體方針。

把它想成同一份食譜交給兩個廚房:食譜沒變,出菜的流程換了。這個概念之所以重要,是因為它決定了「不改程式碼就變快」這句話能不能成立——只要改動全部發生在編譯輸出,使用者手上的原始碼就不必動。也因此,換編譯策略的風險不在語法,而在執行期語意的細微差異。

相關術語: Vapor Mode (實作於)

出處:第 1 段「Vapor Mode 的承諾」

留給下一段 作者把「不用 virtual DOM」當成好處的來源丟了出來,卻還沒說 virtual DOM 現在到底在做什麼、為什麼它會變成成本。要檢驗這張欠條,下一步必須先把現在的 Vue 從 template 到畫面的完整路徑攤開來看。

2. Vue 現在怎麼運作:vdom 全迴圈 0:20–0:56

要懂 Vapor,得先懂現在的 Vue。作者說 Vue 用的是 compiler-informed virtual DOM,先解釋 virtual DOM 這一半:Vue 在 build time 把每個元件的 template 編譯成 render function,執行時配上資料產生 virtual DOM tree,再用真正的 DOM 指令掛上頁面。之後只要 reactive data 一變,render function 就重跑、產生新的 tree,新舊兩棵樹做 diff,最後把差異 patch 回真實 DOM。

template 編譯成 render function 的示意圖,是整條 vdom 流程的第一環,看圖才知道「編譯」發生在哪一步。
0:36 · template 編譯成 render function 的示意圖,是整條 vdom 流程的第一環,看圖才知道「編譯」發生在哪一步。
virtual DOM tree 用真實 DOM 指令 mount 到頁面的畫面,區分「JS 裡的樹」與「瀏覽器裡的元素」。
0:43 · virtual DOM tree 用真實 DOM 指令 mount 到頁面的畫面,區分「JS 裡的樹」與「瀏覽器裡的元素」。
新舊兩棵樹並排 diff 的動畫,是後面「這一步很貴」論證的視覺依據。
0:53 · 新舊兩棵樹並排 diff 的動畫,是後面「這一步很貴」論證的視覺依據。
承上 承接上一段留下的欠條。既然作者主張「拿掉 virtual DOM 就有好處」,就得先看清楚 virtual DOM 在現在的 Vue 裡站在哪個位置、經手哪些工作。這一段就是把那條路徑整條畫出來。

推理因為上一段只給了承諾沒給機制,所以作者接著把 Vue 的執行路徑拆成一條有向的鏈:template 在 build time 被編譯成 render function,render function 配上資料產生 virtual DOM tree,再用真實 DOM 指令 mount 成頁面上的元素。然後他讓 reactive data 從下方接回 render function,把直線變成迴圈:資料一變,render function 重跑、產生新樹、新舊樹 diff、差異 patch 回真實 DOM。他刻意先講完整條迴圈才談成本,因為「成本落在哪一段」得先有這張圖才指得出來。

AI 補充畫面上是一張白板圖,比作者唸出來的內容多給了兩個字,值得停下來看:五個方塊由左到右是 Template →compile→ Render Function →returns→ Virtual Dom Tree →mount/patch→ Actual DOM,而 Reactive Data 掛在下方,用兩條邊接回 Render Function,一條標著 trigger,一條標著 track。 track 這條邊是作者沒說、但整條迴圈少不了的一步:render function 執行的時候,Vue 會記錄它讀到了哪些 reactive data;之後只有那些被讀過的資料變動,才會 trigger 重跑。也就是說 Vue 從來不是「有東西變就全部重畫」,它一直知道「哪個元件該重畫」——只是還不知道「元件裡的哪一塊該重畫」。這個「知道一半」的狀態,是理解後面整條推理的關鍵。 圖上還有一格 `h('div', /*... */)`,那就是 render function 的長相。render function 不回傳 HTML 字串,它回傳一串 h() 呼叫的結果,也就是一棵純 JavaScript 物件組成的樹——這棵樹就是 virtual DOM tree 的實體。左上角那份範例 template(一個 div 裡有 `<span>{{ val }}</span>` 和一個寫死 hi 的 div)會被反覆拿來當例子,記住它會省事很多。 最後,作者說 Vue 有的是 compiler-informed virtual DOM,然後說「我們先談 virtual DOM 這半」——他明說了要把名字的另一半懸著。

compiler-informed virtual DOM

編譯器加持的虛擬 DOM

Vue 的 virtual DOM 實作:因為模板在編譯期就被分析過,runtime 拿得到編譯器留下的提示,不必盲目比對整棵樹。

「compiler-informed」的重點在 informed——被告知。純 runtime 的 virtual DOM 在執行期看到的只有兩棵長得差不多的物件樹,只能一格一格比;Vue 的 runtime 拿到的樹上帶著編譯期標註,知道哪些節點根本不用看。這也是為什麼同樣叫 virtual DOM,Vue 與 React 的成本結構並不相同。

相關術語: render function (編譯產物)、diff (用來省略)

出處:第 2 段「Vue 現在怎麼運作:vdom 全迴圈」

render function

渲染函式

template 編譯後得到的 JavaScript 函式,執行它會產生這個元件當下該有的 virtual DOM tree。

它是整條迴圈的心臟:資料變了,重跑的就是它。理解 Vapor 的關鍵,就在於意識到「重跑整個 render function」是一個以元件為單位的動作——就算模板裡只有一個字改了,這個函式也是整個從頭跑一次。你也可以在 Vue 裡手寫 render function 取代 template,但那樣就放棄了編譯期能給的所有提示。

相關術語: build time (產生於)、reactive data (被它觸發)

出處:第 2 段「Vue 現在怎麼運作:vdom 全迴圈」

build time

建置期

程式碼被打包工具編譯、還沒送到瀏覽器執行的那段時間。

與它相對的是 runtime(執行期,程式在使用者瀏覽器裡跑的時候)。同一件工作放在 build time 做,使用者就完全不用付這筆時間;放在 runtime 做,每個使用者、每次操作都要付一次。前端框架這十年的效能競賽,很大程度就是在比誰能把更多工作往 build time 搬。

相關術語: render function (產出於此)

出處:第 2 段「Vue 現在怎麼運作:vdom 全迴圈」

reactive data

響應式資料

被 Vue 追蹤過的資料:讀它的時候會被記錄,改它的時候會通知所有讀過它的地方。

它是整套機制的觸發源。Vue 用 Proxy 攔截讀寫,所以「誰讀了我」這件事是自動記錄的,不需要開發者宣告依賴。這個自動追蹤能力後來會變成拿掉 virtual DOM 的本錢——因為「誰依賴誰」的資訊,Vue 其實一直都有。

相關術語: render function (觸發它重跑)

出處:第 2 段「Vue 現在怎麼運作:vdom 全迴圈」

diff

差異比對

把新舊兩棵 virtual DOM tree 走一遍、逐一比對,找出真正需要改動的節點。

diff 之後緊接著的動作叫 patch:把找出來的差異用真實 DOM 指令套用到頁面上。這兩個字常被混用,但分開看很有幫助——patch 是無論如何都得做的(畫面總得改),diff 則是「因為不知道哪裡變了,所以只好找」而付出的代價。一旦「哪裡變了」變成已知,diff 就成了純粹的浪費。

相關術語: compiler-informed virtual DOM (在此執行)

出處:第 2 段「Vue 現在怎麼運作:vdom 全迴圈」

留給下一段 整條迴圈已經攤開,但「compiler-informed」的那半個名字還懸著沒解釋。作者留下的線索是一個問題:既然 Vue 多了一道把 template 編譯成 render function 的手續,這道手續除了轉換格式之外,還能拿來做什麼?

3. 編譯期最佳化:static hoisting 0:56–1:27

接著解釋名稱裡「compiler-informed」的那一半,也是 Vue 目前勝過 React 的地方:因為多了「把 template 編譯成 render function」這一步,Vue 可以在編譯期做最佳化。作者舉 static hoisting 為例——template 裡永遠不變的部分,例如寫死 Hi 的那個 div,不會在每次 re-render 都產生新的 virtual node,而是只建立一次,runtime 也知道它永遠不變、不必去 diff 它。這只是其中一項最佳化。

static hoisting 的程式碼示例(固定文字的 div 被提出去只建一次),是「編譯期能做事」這個主張唯一的具體證據。
1:13 · static hoisting 的程式碼示例(固定文字的 div 被提出去只建一次),是「編譯期能做事」這個主張唯一的具體證據。
承上 回應上一段懸著的問題:那道編譯手續除了轉格式還能做什麼。答案是——編譯器可以在編譯期就看出 template 裡哪些部分永遠不會變,並把這個知識交給 runtime,讓 runtime 少做事。

推理因為上一段建立了「template 會先被編譯成 render function」這一步,所以作者接著指出這一步是 Vue 相對於純 runtime 方案的結構性優勢:編譯器手上有整份 template 的靜態文字,可以事先把內容分成「死的」與「活的」兩類。他只舉 static hoisting 一個例子,而且明說「這只是其中一項」、把完整清單丟到說明欄——因為他真正要建立的不是「Vue 有幾項最佳化」,而是「編譯期知道得比 runtime 多」這個觀念。這個觀念一旦成立,後面的推論才有立足點。

AI 補充這一段的截圖把上一段那份範例 template 畫成了節點樹,而且用視覺直接標出編譯器的分類結果:根節點 div 分成兩支,左邊是 p → span → `{{ val }}`,最底下那個 `{{ val }}` 是綠色的;右邊是 div → 文字 "hi",整支被一個框框起來。綠色代表動態、框起來的代表被 hoist 出去。也就是說,這棵樹在編譯完成的那一刻就已經被塗上顏色了,runtime 只要照著顏色做事。 有一個區分作者沒講,但不講清楚很容易誤會:static hoisting 省掉的是「重新建立 virtual node 物件」和「比對它」的成本,不是省掉真實的 DOM 元素。頁面上那個 `<div>hi</div>` 一樣存在、一樣佔記憶體,被提出去只建一次的是它在 JavaScript 那邊的分身。這個區分之後在估算「拿掉 virtual DOM 到底省下什麼」時很關鍵——省的一直是分身那一側的成本。 另外他說的「其他最佳化」不是虛指,patch flag、block tree 都屬於這一類:全部都是同一個套路——編譯器把它看出來的事實編碼進輸出,讓 runtime 不必在執行期重新推導一次。

術語:compile-time optimization

static hoisting

靜態提升

編譯器把模板中永遠不變的片段提到 render function 外面,只建立一次、之後每次渲染直接重用。

hoist 是「吊起來」的意思:把那段程式碼從每次都會執行的函式體裡吊到只執行一次的模組層。它的效益隨著模板裡靜態內容的比例上升——一個滿是版面結構、只有幾個字會變的頁面,能被提出去的部分可能占絕大多數。要注意它處理的是「結構與內容都固定」的片段,只要裡面有任何一個綁定,整個片段就沒辦法被提升。

相關術語: virtual node (減少建立)、compile-time optimization (一種)

出處:第 3 段「編譯期最佳化:static hoisting」

virtual node

虛擬節點(v-node)

virtual DOM tree 裡的一個節點物件,描述一個元素的標籤、屬性與子節點。

常縮寫成 v-node 或 vnode。每一個畫面上的元素在更新時都會有一個對應的 v-node 被建立出來,這些物件是短命的:建立、比對、丟棄,交給 GC。數量一大,它們就同時是 CPU 成本(配置與走訪)和記憶體成本(暫時佔用與回收壓力),這兩筆帳之後會分別對應到「更快」和「更省記憶體」兩個承諾。

相關術語: static hoisting (被它省去)

出處:第 3 段「編譯期最佳化:static hoisting」

runtime

執行期

程式實際在使用者瀏覽器裡跑的那段時間,也指為此必須一起下載的那份框架程式碼。

這個詞有兩個意思,這裡兩個都用得上:作為時間,它是與 build time 相對的階段;作為東西,「Vue 的 runtime」指的是隨你的 app 一起送到瀏覽器的那包框架程式碼。後者是可被丈量的成本——使用者要下載它、解析它、執行它,所以「能不能把某一塊 runtime 整個拿掉」會是一個很實際的問題。

相關術語: build time (相反)

出處:第 3 段「編譯期最佳化:static hoisting」

compile-time optimization

編譯期最佳化

在編譯階段就把可以確定的事算好或標註起來,讓執行期少做工作的手法總稱。

它的本質是把資訊從編譯器搬到 runtime。編譯器看得到整份模板的靜態結構,runtime 看得到實際的資料——最佳化的空間就在兩者之間:凡是編譯期就能確定的,都不該留到執行期再算。Vue 的 static hoisting、patch flag 都是這個套路,而 Vapor 可以看成把這個套路推到底:連「哪個資料影響哪個節點」也在編譯期定死。

相關術語: static hoisting (包含)、build time (發生於)

出處:第 3 段「編譯期最佳化:static hoisting」

見仁見智 0:59
「that tools like react don't or at least don't do yet」
在 2024 年 4 月成立,但這條界線後來被 React Compiler 模糊掉了:React 也有了編譯步驟,會自動做 memoization 並提升靜態的 JSX 元素。要注意兩者仍不等價——React Compiler 省的是「元件函式重跑與 re-render」,Vue 省的是「virtual node 的建立與比對」,切入的層級不同。所以這句話今天不能再當成「只有 Vue 有編譯期最佳化」來讀,但也不能反過來說 React 已經追平。
依據: React Compiler(2024 年推出 beta/RC,之後隨 React 19 生態普及)
留給下一段 作者已經把「編譯期知道得比 runtime 多」立成事實,但他用的是防守姿態(Vue 比 React 做得多)。留給下一段的是一個他還沒問出口的問題:編譯期最佳化只削掉了模板裡靜態的那一半,剩下真的會變的那一半,還是得走一次完整的建樹與比對——這筆帳能不能也省掉?

4. vdom 的代價,與拿掉它的念頭 1:27–2:01

推理的轉折點。作者指出就算有編譯器幫忙,virtual DOM 仍有無法消除的 overhead:它得佔記憶體,資料一變就得重建一堆 v-node,還得走完整棵樹找差異。於是他提出反問——如果 reactive data 改變時,我們早就知道 DOM 的哪幾個位置會受影響呢?那就可以直接雷射般打進那個點改掉它,不必重跑整個 render function、不必 diff 整棵樹,甚至根本不需要這棵 virtual DOM tree。他用 val 改變只更新那個 span 為例:這就是 Vue Vapor Mode。

「laser into those spots」的示意動畫:直接命中受影響節點,與前一段整棵樹 diff 形成 before/after 對照。
1:44 · 「laser into those spots」的示意動畫:直接命中受影響節點,與前一段整棵樹 diff 形成 before/after 對照。
val 改變、直接對那個 span 下 DOM 指令更新文字的畫面,是 Vapor Mode 概念的定義性一格。
1:58 · val 改變、直接對那個 span 下 DOM 指令更新文字的畫面,是 Vapor Mode 概念的定義性一格。
承上 接住上一段留下的問題:編譯期最佳化只處理得了靜態的那一半,動態的那一半仍要付出建立 v-node 與走訪整棵樹的成本。作者從這裡把矛頭從「Vue 比誰好」轉向 virtual DOM 本身。

推理因為上一段證明了編譯器能事先知道很多事,所以作者接著把這個能力推到極限:如果編譯器連「哪個資料影響哪個 DOM 節點」都知道,那 diff 就是在重新計算一個早就已知的答案。他的論證分成三拍——先列出最佳化削不掉的固定成本(要佔記憶體、資料一變就要建一堆 v-node、要走完整棵樹找差異),再用一句反問把前提整個換掉(如果我們早就知道哪裡會變呢),最後讓結論自己掉出來:不只不用重跑 render function、不用 diff,連這棵樹本身都不需要。這是整支影片的轉折點,而它的說服力完全建立在前兩段先把迴圈畫清楚上。

AI 補充這兩張截圖是同一張圖的 before / after,資訊比作者唸出來的更精確,值得逐格看: 第一格裡,上一段那張流程圖被加了註記——Virtual Dom Tree 方塊的正上方多了一個時鐘圖示和一個像磁碟/堆疊的圖示,正是他口中「要花時間走訪」和「要佔記憶體」這兩筆帳,被畫在同一個方塊頭上;左下角多了「Compile Time Optimizations」的字樣,用一條線指向 compile 那一段,等於承認前一段講的最佳化只作用在這條邊上;而 Reactive Data 這時多拉出一條虛線,越過中間所有方塊直接接到 Actual DOM。那條虛線就是他正在提出的新路徑,此刻還與舊路徑並存。 第二格是決定性的一格:Virtual Dom Tree 這個方塊從圖上消失了。剩下的只有 Render Function —mount→ Actual DOM,以及 Reactive Data —patch(虛線)→ Actual DOM。這張圖把 Vapor 的定義說得比任何一句話都清楚:mount 仍然要走一次,畫面總得先被建立出來;被拿掉的是之後每一次更新的中介——更新時資料直接 patch 到真實 DOM,中間沒有任何一棵樹。 順著這張圖可以補上作者第一句承諾的來源。省下的不只是 diff 的 CPU 時間,還有那些短命 v-node 物件的配置與回收——這就是「use less memory」那半句的出處,也正是圖上那個磁碟圖示標的東西。 另外「不需要重跑整個 render function」這句要小心讀。Vapor 不是沒有初始化程式碼,它一樣有一次性的建立階段;被取消的是「每次更新都重跑一個會產生整棵樹的函式」這個習慣動作。

術語:vdom overheadfine-grained updateDOM command

vdom overhead

虛擬 DOM 的額外開銷

為了維持 virtual DOM 這層中介而必須付出、且無法用編譯期最佳化消除的固定成本。

它由三筆帳組成:樹要存在記憶體裡、更新時要建立大量 v-node、比對時要走訪整棵樹。前一段的最佳化能削掉其中屬於靜態內容的部分,但只要模板裡還有動態綁定,這三筆帳就削不到零。認清「這是固定成本而非可優化成本」,是作者敢主張整層拿掉的前提。

相關術語: diff (主要來源)、fine-grained update (被它取代)

出處:第 4 段「vdom 的代價,與拿掉它的念頭」

fine-grained update

細粒度更新

資料變動時只更新真正受影響的那一個 DOM 節點,不重算也不比對其他部分。

「粒度」指的是「一次更新的最小單位」。現在的 Vue 粒度是元件:一個 ref 變了,整個元件的 render function 重跑,再靠 diff 把範圍縮回去。細粒度更新則把單位縮到單一節點甚至單一屬性,所以根本不需要事後縮小範圍。代價是編譯器必須在編譯期就把每個動態位置與它的來源綁死,因此對動態程度很高的模板反而比較難處理。

相關術語: vdom overhead (為了消除它)

出處:第 4 段「vdom 的代價,與拿掉它的念頭」

DOM command

DOM 操作指令

瀏覽器提供的、直接修改頁面的原生操作,例如設定文字內容、新增或移除元素。

無論用哪個框架,最後真正改變畫面的都是這些指令——差別只在「誰、在什麼時候、根據什麼決定要下哪幾道」。virtual DOM 的做法是先在 JavaScript 裡算出一份指令清單再送出去;Vapor 的做法是編譯期就把指令與資料綁好,資料一變就直接送。這也說明為什麼拿掉 virtual DOM 不會讓畫面更新變得不安全:最終出手的還是同一批指令。

相關術語: fine-grained update (執行手段)

出處:第 4 段「vdom 的代價,與拿掉它的念頭」

留給下一段 結論已經下了,但它整個押在一個尚未兌現的前提上:「資料變的時候,我們早就知道哪些 DOM 會受影響」。作者到這裡一個字都還沒說 Vue 憑什麼知道。這個空缺就是下一段必須補上的。

5. 做得到的原因:reactivity system 2:01–2:19

為什麼 Vue 有本錢拿掉 virtual DOM?作者的答案是 Vue 既有的 reactivity system:可以讓 DOM 的一小塊直接訂閱某個 reactive state,那個 ref 一變,就只有這一小塊需要更新。他接著說 Vapor Mode 仍在開發中,但 repo 已公開,還有一個 playground 可以玩,於是帶出下一段的實例。

DOM 節點訂閱 reactive state 的連線示意圖,說明「知道哪裡會變」不是魔法而是既有機制。
2:08 · DOM 節點訂閱 reactive state 的連線示意圖,說明「知道哪裡會變」不是魔法而是既有機制。
承上 補上一段留下的空缺:憑什麼「早就知道哪裡會變」。作者的答案不是新發明,而是 Vue 本來就有的 reactivity system——第 2 段那張圖上標的 track 與 trigger 兩條邊,畫的就是它。

推理因為上一段的結論完全押在那個前提上,所以作者接著把前提還原成既有機制:reactivity system 一直在追蹤「誰讀了這個 ref」,只是在現在的 Vue 裡,被登記的訂閱者是整個元件的 render function。他要主張的其實只有一件事——把訂閱的單位從「元件」縮小到「單一 DOM 節點」,上一段那條從資料直達真實 DOM 的虛線就成立了。這個推理的漂亮之處在於它不需要任何新技術,只需要換一個訂閱粒度,也因此他能很自然地接著說「repo 已經公開、還有 playground」,把話題從原理帶向證據。

AI 補充這一段的截圖是一張只有四行的投影片,而且作者特地在第一行寫了「不是真的程式碼,但概念類似」: ```js watch([myDep], () => { node.nodeValue = myDep.value }) ``` 這四行把整個 Vapor 的心智模型壓縮完了:一個訂閱綁著一個 ref,被觸發時做的事就是把某個真實 DOM 節點的文字直接寫掉。沒有樹、沒有比對、沒有回傳值可言。他用 watch 只是為了讓人看得懂形狀。 真正要盯住的是 `node` 這個變數。它是一個已經拿在手上的真實 DOM 節點參照——而「這個參照從哪裡來」,正是 Vapor 整套設計裡最硬的工程問題:編譯器必須在編譯期替模板裡每一個會變動的位置,產生一個可以穩定拿到的節點參照。作者在這裡沒有提,但這是把偽程式碼變成可用編譯器的關鍵一步。 還有一點值得補:這一段真正的主張是「粒度」,不是「機制」。同一套 reactivity,粒度粗(元件層級)就必須靠 diff 事後找出實際變的地方;粒度細(節點層級)就不需要中介。這也正是 Solid JS(見第 1 段)一直以來的做法——所以 Vapor 借的不是實作,是這個訂閱單位的選擇。

術語:effect

reactivity system

響應式系統

Vue 用來追蹤「哪段程式碼讀了哪個資料」、並在資料變動時通知它們的整套機制。

它由兩個動作構成:讀取時收集依賴(track),寫入時通知訂閱者(trigger)。Vue 3 用 Proxy 實作,所以這件事完全自動、不需要開發者宣告。它最容易被忽略的一點是:這套機制本身跟 virtual DOM 沒有任何關係,是兩個可以拆開的東西——正因為可以拆開,Vapor 才有辦法留下前者、拿掉後者。

相關術語: ref (基本單位)、effect (訂閱者)

出處:第 5 段「做得到的原因:reactivity system」

ref

響應式參考值

Vue 中最基本的響應式容器,把一個值包起來,透過 .value 讀寫並被追蹤。

之所以要用 `.value` 而不是直接放一個變數,是因為 JavaScript 無法攔截對一個普通變數的讀寫——必須有一個物件當中介,Proxy 才有東西可以攔。這個看起來囉嗦的設計正是自動依賴追蹤的代價。也因為每個 ref 都是獨立的追蹤單位,「某個節點只訂閱某個 ref」在技術上才有意義。

相關術語: reactivity system (被它追蹤)

出處:第 5 段「做得到的原因:reactivity system」

effect

副作用函式

一段會被自動記錄依賴的函式:它讀過的 reactive data 一旦改變,它就會被重新執行。

現在的 Vue 裡,元件的 render function 本身就是一個 effect——只是它很大,涵蓋整個元件。把 effect 拆小,小到一個 effect 只負責一個節點的一段文字,就是這一段講的粒度轉換。watch、computed、watchEffect 都是包裝過的 effect,差別只在觸發時機與是否有回傳值。

相關術語: reactivity system (運作於)、fine-grained update (實作手段)

出處:第 5 段「做得到的原因:reactivity system」

留給下一段 原理到這裡齊了,但整條推理至今全部停留在白板方塊與四行偽程式碼上。作者自己把下一步指了出來:repo 是公開的、還有一個 playground——線索很具體,就是「同一個元件,把 Vapor 開關切一下,編譯出來的程式碼到底長什麼樣」。

6. Playground 對照:關掉與打開 Vapor 2:19–3:14

作者用 playground 的三個小例子(可切換的 H1、綁 input 的訊息、計數器)比對編譯結果。Vapor 關閉時:一個 setup function 建立 refs,加上一個 render function,資料一變就整個重跑,用 createElementBlock、createElementVNode 這類函式組出 virtual DOM tree。Vapor 打開後:setup function 還在,但回傳的不是 render function,而是一個自執行函式;裡面是一堆 n 開頭的變數,用 HTML 字串和 template 方法建立真正的 DOM 節點,v-if 用 watcher 監看 show.value,動態文字用 effect 綁在對應的 ref 上,再加上處理 directive 與事件的 helper。作者說即使不完全看懂這段程式碼,它也很合理:UI 的每一塊都精準地由影響它的那個 ref 控制。

Vapor 關閉時的編譯輸出:setup function 與 render function 並列,是對照組的基準畫面。
2:31 · Vapor 關閉時的編譯輸出:setup function 與 render function 並列,是對照組的基準畫面。
createElementBlock / createElementVNode 出現在輸出裡,證明「組出 virtual DOM」不是比喻而是實際呼叫。
2:38 · createElementBlock / createElementVNode 出現在輸出裡,證明「組出 virtual DOM」不是比喻而是實際呼叫。
Vapor 打開後的自執行函式與 n0、n1 等節點變數,是本片最關鍵的 before/after 對照的另一半。
2:49 · Vapor 打開後的自執行函式與 n0、n1 等節點變數,是本片最關鍵的 before/after 對照的另一半。
動態文字用 effect 綁在對應 ref 上的那幾行,把第 5 段的 reactivity 說法落實成看得見的程式碼。
3:01 · 動態文字用 effect 綁在對應 ref 上的那幾行,把第 5 段的 reactivity 說法落實成看得見的程式碼。
承上 接住上一段的邀請,把說法拿到真實的編譯輸出前面驗一次。上一段那四行偽程式碼主張「一個 effect 直接寫某個 node」,這一段要看的就是真正的編譯器有沒有產出那個形狀。

推理因為前面所有推論都停在示意圖層級,所以作者選了最省力也最有說服力的驗證方式:同一份 App.vue,只切 VAPOR OFF / VAPOR ON 這一顆開關,把兩邊的 JS 輸出並排看。他挑的範例也不是隨手寫的——一個用 v-if 切換的 h1、一個用 v-model 綁 input 的訊息、一個 counter,剛好蓋住條件渲染、雙向綁定、事件與動態文字這四種最常見的動態。只要這幾種在 Vapor 輸出裡都找得到對應的機制,前面的推理就算落了地。

AI 補充這是全片資訊密度最高的一段,四張截圖各自扛著不同的證據,值得逐張讀。 **關閉 Vapor(第一張)**:右側輸出是 `const __sfc__ = { __name: 'App', setup(__props) { … } }`,setup 裡是原封不動的三個 ref 與 increment 函式,接著 `return (_ctx, _cache) => { return (_openBlock(), _createElementBlock(_Fragment, …` ——setup 回傳的是一個函式。這一格就是第 2 段那張圖裡「Template →compile→ Render Function」的實體,抽象方塊在這裡變成了看得見的程式碼。 **關閉 Vapor、往下捲(第二張)**:`_createElementVNode("button", { onClick: … }, "Toggle H1")`、`(show.value) ? (_openBlock(), _createElementBlock("h1", _hoist…)) : _createCommentVNode("v-if", true)`、`_withDirectives(… "input" …, 512 /* NEED_PATCH */, [[_vModelText, msg.value]])`,結尾是 `], 64 /* STABLE_FRAGMENT */)`。這裡有三件事值得指出來:其一,v-if 為 false 時輸出的不是「什麼都沒有」,而是一個註解節點佔位——因為 diff 需要新舊兩棵樹的位置對得上,位置不能憑空消失。其二,`_hoist…` 就是第 3 段講的 static hoisting,真的出現在輸出裡。其三,`512 /* NEED_PATCH */`、`64 /* STABLE_FRAGMENT */` 這些數字就是第 3 段說的「其他最佳化」的實體,它們是編譯器留給 runtime 的提示,告訴它哪些部分可以跳過不比。同時,這一整片 `_create*` 呼叫也讓 vdom 的成本有了具體形狀:每一次更新,這些呼叫全部要再跑一遍。 **打開 Vapor(第三張)**:setup 一字不差還在,但 return 的不再是 `(_ctx, _cache) =>`,而是 `(() => { … })()` 這個自執行函式,裡面是 `const n0 = t0()`、`const n6 = t2()`、`const n4 = n6.firstChild`、`const n5 = n4.nextSibling`。這正是第 5 段那個 `node` 變數的真身:`t0`、`t2` 是用 HTML 字串一次做出來的模板、呼叫時 clone 一份,而個別節點的參照不是一個一個 createElement 出來的,是靠 `firstChild` / `nextSibling` 在剛 clone 出來的結構上走出來的——這比逐一建立元素便宜得多。作者說「一堆 n 開頭的變數,我猜是不同的節點」,猜對了。 **打開 Vapor、往下捲(第四張)**:`_withDirectives(n4, [[_vModelText, () => msg.value]])`、`_delegate(n0, "click", …)`、`const n1 = _createIf(() => (show.value), () => { const n3 = t1(); _renderEffect(() => _setText(n3, msg.value)); return n3 })`、`_delegate(n5, "click", () => increment)`、`_renderEffect(() => _setText(n5, " Increment ", count…))`,最後 `return [n0, n1, n6]`。那行 `_renderEffect(() => _setText(n3, msg.value))` 就是第 5 段那四行偽程式碼的實作,形狀幾乎一模一樣。 作者有兩處說法可以再精確一點。他說 v-if「用的是類似 watcher 監看 show.value 的東西」,實際輸出是 `_createIf(() => show.value, …)`——它是一個分支原語,除了監看之外還要負責子節點區塊的建立、插入與銷毀,責任比 watcher 重。他說動態文字「用 effect 綁在對應的 ref 上」,實際是 `_renderEffect`,它比一般 effect 多了與渲染排程對齊的語意(更新會被批次到同一個時機執行,避免一幀之內寫好幾次 DOM)。至於他一句帶過的「處理 directive 與事件的其他 helper」,其中 `_delegate` 值得單獨拿出來:事件不是綁在每個節點上,而是委派到共同的祖先,這是 Vapor 另一項省記憶體的手段。 最後一個容易被忽略的細節:畫面右上角的 Vue Version 顯示的是 `@db140a1`,一個 commit 雜湊而不是版本號——說明這個 playground 是從 vapor 分支建出來的,正好呼應「還在開發中」這件事。

術語:patch flagrenderEffectevent delegationtemplate cloning

patch flag

更新標記

編譯器寫在 v-node 上的數字標記,告訴 runtime 這個節點只有哪一類內容會變。

輸出裡那些 `512 /* NEED_PATCH */`、`64 /* STABLE_FRAGMENT */` 就是它,用位元旗標編碼,所以可以用一次位元運算判斷。有了它,runtime 比對一個節點時不必檢查所有屬性,只看標記指出的那幾項。它是「編譯器把知道的事告訴 runtime」這個套路最典型的例子——也說明現在的 Vue 已經在往「事先知道哪裡會變」的方向走,只是還隔著一棵樹。

相關術語: compile-time optimization (一種)、diff (用來加速)

出處:第 6 段「Playground 對照:關掉與打開 Vapor」

renderEffect

渲染副作用

Vapor 輸出裡用來把一個 DOM 更新綁定到 reactive data 的 effect,資料一變就直接執行那道 DOM 操作。

它與一般 effect 的差別在排程:多個 renderEffect 在同一輪資料變動中會被批次到同一個時機執行,避免同一幀之內對 DOM 寫好幾次。實務上它就是 Vapor 版本的「更新單位」——現在的 Vue 一個元件配一個渲染 effect,Vapor 則是一個動態位置配一個。看懂這一行,等於看懂了整個 Vapor。

相關術語: effect (一種)、fine-grained update (實作於)

出處:第 6 段「Playground 對照:關掉與打開 Vapor」

event delegation

事件委派

不在每個元素上各綁一個事件監聽器,而是綁在共同祖先上,靠事件冒泡判斷實際來源。

輸出裡的 `_delegate(n0, "click", …)` 就是它。好處是監聽器數量與元素數量脫鉤——一個一千列的表格不會產生一千個監聽器,這在記憶體上是實打實的節省。它在原生 JavaScript 時代就是常用技巧,Vapor 把它變成編譯器的預設行為,開發者寫的還是 `@click`。

相關術語: DOM command (相關)

出處:第 6 段「Playground 對照:關掉與打開 Vapor」

template cloning

模板複製

把一段 HTML 字串先做成一份模板,每次要用時整份 clone 下來,而不是逐個元素建立。

輸出裡的 `t0()`、`t1()`、`t2()` 就是這種模板函式。瀏覽器複製一整棵已解析好的結構,比執行幾十次 createElement 加 appendChild 快上不少,因為結構解析只做一次。代價是拿到 clone 之後還得走 `firstChild` / `nextSibling` 找出需要操作的節點——這些走訪路徑同樣是編譯期就算好寫死的。

相關術語: DOM command (替代做法)

出處:第 6 段「Playground 對照:關掉與打開 Vapor」

留給下一段 機制與證據都齊了。但這兩份輸出差異之大,本身就丟出了下一個問題:長成這樣的程式碼,要怎麼跟現有那些已經寫好的元件在同一個 app 裡共存?作者留下的線索是他這段結尾那句「即使不完全看懂也覺得合理」——技術上說得通之後,接著該問的是它怎麼被放進真實專案裡。

7. 整合計畫、限制與結語 3:14–4:08

作者收尾談落地方式。他欣賞 Vue 團隊從 Vue 2 到 Vue 3 的轉換學到教訓:Vapor 不會影響任何既有 app,完全是 opt-in,而且以元件為單位、用類似 .vapor 的寫法啟用,因此同一個 app 裡可以自由混用 Vapor 元件與現在的元件。假設整個 app 都是 Vapor 元件,virtual DOM runtime 就會從 bundle 裡被移除,讓瀏覽器要下載與執行的 runtime 程式碼變少。但他也留下未解的問題:Vapor 與非 Vapor 元件之間的功能對等程度未知,目前已知 Vapor 元件只支援 Composition API 與 script setup,其他限制還不清楚。

以元件為單位、用 .vapor 標記啟用的寫法示意,是「怎麼混用」這個實務問題唯一具體的畫面答案。
3:32 · 以元件為單位、用 .vapor 標記啟用的寫法示意,是「怎麼混用」這個實務問題唯一具體的畫面答案。
承上 接住上一段留下的共存問題:既然 Vapor 的編譯輸出和現在的輸出根本是兩種東西,兩種元件要怎麼放在同一個 app 裡。作者的答案是把選擇權下放到單一元件。

推理因為上一段展示的兩份輸出差距太大,讓「整包換過去」聞起來像另一次 Vue 2 → Vue 3,所以作者接著把重心放在遷移策略而不是效能數字:不影響既有 app、完全 opt-in、以元件為單位啟用、可自由混用。他的推理是從歷史教訓倒推設計——Vue 2 到 3 最痛的地方是全有全無,所以這次要把粒度細到單一檔案。也正因為先講清楚了混用機制,他才敢接著談那個最大的好處(整包都是 Vapor 元件就能把 vdom runtime 從 bundle 裡移除)——那是一個條件句,得先知道混用有多容易,才知道這個條件有多難達成。最後他把沒有答案的部分誠實列出來,讓整支影片停在「值得放進雷達」而不是「現在就換」。

AI 補充這一段的截圖有點反諷:抽幀正好落在動畫還只顯示 `MyButton.vue` 的那一格,作者口中那個標記還沒加上去。這反而提醒了一件事——當時那個寫法本來就只是提案中的候選之一,作者自己也用「something like」帶著保留在講。 有一個推論作者略過了,但它決定你現在該怎麼看待 Vapor 的收益:「整個 app 都是 Vapor 元件才能移除 vdom runtime」是個非常硬的條件。只要用到任何一個依賴 virtual DOM 的第三方元件庫、任何一個還沒轉換的舊元件,這個好處就完全消失。所以在可預見的期間內,Vapor 的實際收益主要來自熱點——大型清單、頻繁更新的表格、動畫密集的介面——而不是整體 bundle 縮小。把它當成「針對痛點的局部工具」比當成「全域效能開關」更接近現實。 opt-in 的代價也值得指出來,因為它正是作者那句 feature parity 疑慮的具體來源:兩套執行模型並存,等於 Vue 要長期維護兩條 runtime,而最容易出問題的地方一定是模式交界——Vapor 父元件裡放非 Vapor 子元件、或反過來,slot、provide/inject、transition 這些跨越元件邊界的機制要怎麼在兩種模型之間對接,都是真正的難題。 至於他提到只支援 Composition API 與 script setup,這不是任性的限制,而是模型衝突:Options API 依賴 `this` 指向元件實例、依賴執行期才組裝起來的選項物件,這與「編譯期就把每個動態位置和它的來源綁死」的做法天生不相容。換句話說,這條限制不太可能靠後續開發解除。

opt-in

選擇性啟用

預設不啟用、由使用者主動選擇才生效的機制,因此不會影響任何既有程式碼。

它的反面是 breaking change(破壞性變更)——升級後不改就會壞。opt-in 是框架推大改動時降低阻力的標準做法,代價是新舊兩套機制要長期並存,維護成本轉嫁到框架自己身上。判斷一個 opt-in 設計好不好,主要看粒度:能以單一檔案為單位選擇,遠比以整個專案為單位容易被採用。

相關術語: feature parity (帶來疑慮)

出處:第 7 段「整合計畫、限制與結語」

feature parity

功能對等

兩套並存的實作在功能上是否一樣完整、能不能互相替換。

對 Vapor 來說這是最實際的採用門檻:如果某個 Vapor 元件不能用 slot、不能用某個內建元件,「自由混用」就只是理論上的。要注意功能對等不只是「有沒有這個功能」,還包括邊界行為是否一致——生命週期觸發順序、更新時機、錯誤傳播。作者在影片裡把它列為未解問題,是誠實的做法。

相關術語: opt-in (前提)

出處:第 7 段「整合計畫、限制與結語」

Composition API

組合式 API

Vue 3 的寫法:在 setup 裡用 ref、computed 等函式組出元件邏輯,不依賴 this。

它把元件邏輯從「掛在實例上的一堆選項」變成「一組普通的函式呼叫」,因此依賴關係在程式碼裡是明寫出來的、可以被靜態分析。這正是 Vapor 只支援它的原因:編譯器要在編譯期看出哪個 ref 影響哪個節點,就必須讀得懂資料的來源。

相關術語: Options API (相反)、script setup (常搭配)

出處:第 7 段「整合計畫、限制與結語」

script setup

setup 語法糖

單檔元件裡的編譯期語法糖,讓 script 區塊本身就是 setup 函式,頂層變數自動暴露給 template。

它是純編譯期的東西,最終還是被編譯成一般的 setup 函式。對 Vapor 而言它的價值不在少打幾個字,而在於它讓 script 與 template 之間的綁定關係在編譯期是完全確定的——編譯器知道 template 裡的 msg 就是 script 頂層那個 ref,不需要在執行期查找。

相關術語: Composition API (語法糖)

出處:第 7 段「整合計畫、限制與結語」

Options API

選項式 API

Vue 傳統寫法:用 data、methods、computed 等選項描述元件,內部靠 this 取用。

它的可讀性對新手友善,缺點是邏輯依關鍵字分散、難以抽取複用。對編譯器來說更麻煩的是 `this` ——一個屬性到底來自 data、computed 還是 mixin,往往要到執行期把選項物件組裝完才知道,這與「編譯期把動態位置定死」的需求正面衝突。這也是為什麼這條限制不太可能靠後續開發解除。

相關術語: Composition API (相反)

出處:第 7 段「整合計畫、限制與結語」

確定錯誤/已過時 3:14
「even though it's still in development and not ready to use yet」
這句在影片發布的 2024 年 4 月成立,今天已經過時。Vapor Mode 後來隨 Vue 3.6 進入正式的版本線,不再只是一個要自己 build 的實驗分支——影片裡那個顯示 commit 雜湊的 playground 也已經換成正式版本號。要看 Vapor 現在的狀態與可用範圍,請以 Vue 官方 3.6 的文件為準,不要照這支影片的結論以為它還不能碰。
依據: Vue 3.6(Vapor Mode 隨核心釋出,2025 年起提供)
見仁見智 3:30
「to happen at the component level using something. vapor.」
作者自己用「something like」保留過,而最後定案的啟用方式與這裡描述的檔名慣例不同:實際是在單檔元件的 script 區塊上加一個 vapor 標記(`<script setup vapor>`),而不是把檔案改名成 .vapor.vue。「以元件為單位、可與現有元件混用」這個大方向沒有變,變的只是寫法。看這段時請把重點放在粒度,不要把那個檔名當成 API。
依據: Vue 3.6 的 vapor 啟用方式(script 區塊屬性)
留給下一段 總結收束。

4. 總結

作者的推導是一條先立承諾、再拆成本、最後還原機制的鏈。他先丟出一個可檢驗的承諾:元件一行都不用改,app 卻更快、更省記憶體——因為執行時不再使用 virtual DOM。但這張欠條要兌現,得先知道 virtual DOM 現在在做什麼,所以他把 Vue 的執行路徑整條攤開:template 在 build time 編譯成 render function,render function 產生 virtual DOM tree,再用真實 DOM 指令掛上頁面;reactive data 一變就重跑整條路徑,新舊兩棵樹 diff 之後 patch 回真實 DOM。接著他先替 Vue 說好話,用 static hoisting 說明「編譯期知道得比 runtime 多」——這句誇獎其實是為了讓反面論證更有力:連做了編譯期最佳化的 virtual DOM 都還有削不掉的固定成本,要佔記憶體、資料一變就要重建一堆 v-node、還要走完整棵樹找差異。成本落點確定後,他用一句反問換掉整個前提:如果資料改變時我們早就知道 DOM 的哪幾個位置會受影響呢?那 diff 就是在重算一個已知的答案,於是不只 render function 不用重跑、樹不用比對,連 virtual DOM tree 本身都可以不要——這就是 Vapor Mode。這個結論押在「早就知道哪裡會變」這個前提上,所以他立刻把前提還原成 Vue 既有的 reactivity system:同一套追蹤機制,只要把訂閱粒度從整個元件的 render function 縮小到單一 DOM 節點,前提就成立,不需要任何新發明。理論齊了他就換成證據模式,用 SFC Playground 把同一份 App.vue 在 VAPOR OFF 與 VAPOR ON 下的編譯輸出並排:一邊是 createElementVNode 組出來的樹,一邊是 n0、n1 這些真實節點變數加上 renderEffect 直接寫文字,前面所有說法在這裡從示意圖變成程式碼。最後他從技術回到工程現實:完全 opt-in、以元件為單位啟用、可與現有元件混用;好處的兌現條件是整包都是 Vapor 元件才能把 vdom runtime 從 bundle 移除;還沒有答案的是功能對等程度與 Composition API 之外的限制。

勘誤總整理

確定錯誤/已過時見仁見智(取決於版本或情境)

段落原話(transcript 逐字)說明
7. 整合計畫、限制與結語
3:14
「even though it's still in development and not ready to use yet」這句在影片發布的 2024 年 4 月成立,今天已經過時。Vapor Mode 後來隨 Vue 3.6 進入正式的版本線,不再只是一個要自己 build 的實驗分支——影片裡那個顯示 commit 雜湊的 playground 也已經換成正式版本號。要看 Vapor 現在的狀態與可用範圍,請以 Vue 官方 3.6 的文件為準,不要照這支影片的結論以為它還不能碰。
依據: Vue 3.6(Vapor Mode 隨核心釋出,2025 年起提供)
3. 編譯期最佳化:static hoisting
0:59
「that tools like react don't or at least don't do yet」在 2024 年 4 月成立,但這條界線後來被 React Compiler 模糊掉了:React 也有了編譯步驟,會自動做 memoization 並提升靜態的 JSX 元素。要注意兩者仍不等價——React Compiler 省的是「元件函式重跑與 re-render」,Vue 省的是「virtual node 的建立與比對」,切入的層級不同。所以這句話今天不能再當成「只有 Vue 有編譯期最佳化」來讀,但也不能反過來說 React 已經追平。
依據: React Compiler(2024 年推出 beta/RC,之後隨 React 19 生態普及)
7. 整合計畫、限制與結語
3:30
「to happen at the component level using something. vapor.」作者自己用「something like」保留過,而最後定案的啟用方式與這裡描述的檔名慣例不同:實際是在單檔元件的 script 區塊上加一個 vapor 標記(`<script setup vapor>`),而不是把檔案改名成 .vapor.vue。「以元件為單位、可與現有元件混用」這個大方向沒有變,變的只是寫法。看這段時請把重點放在粒度,不要把那個檔名當成 API。
依據: Vue 3.6 的 vapor 啟用方式(script 區塊屬性)

5. 推薦三個下一步

1. 往下挖深:Vue reactivity 的追蹤機制

整支影片的結論押在「資料變時早就知道哪個 DOM 節點受影響」這個前提上,而作者只用一句 reactivity system 帶過。要真正相信這條推論,得看懂 Proxy 怎麼在讀取時 track、寫入時 trigger,以及 effect 的訂閱粒度為什麼可以縮到單一節點。

YouTube 搜尋:Vue 3 reactivity explained proxy track trigger how does Vue reactivity work under the hood signals vs virtual dom fine-grained reactivity

2. 往旁邊對照:其他無 virtual DOM 的框架

作者說 Vapor 的靈感來自 Solid JS,但沒說 Solid 怎麼做、也沒比較 Svelte 這條同樣走編譯路線卻做法不同的路。看過對照組,才分得出哪些是 Vapor 的設計選擇、哪些是這一整類方案的共同代價。

YouTube 搜尋:SolidJS fine-grained reactivity no virtual DOM Svelte compiler vs virtual DOM Solid vs Svelte vs Vue Vapor comparison

3. 往上應用:實際跑一次 Vapor 並量效能

影片只給了 Playground 的編譯輸出,沒有任何實測數字,「更快、更省記憶體」到目前為止仍是承諾。自己開一個 Vapor 專案、用 DevTools 的 Performance 與 Memory 面板量 bundle 大小與更新耗時,才知道這筆好處在自己的場景值多少。

YouTube 搜尋:Vue Vapor Mode tutorial setup Vue 3.6 vapor benchmark performance Chrome DevTools memory profiling JavaScript framework

📄 全部影片 · 主題區: 歷史參考(已過時)