Vue 3 的響應式引擎:用 effect / track / trigger 與三層 targetMap 從零把依賴存起來

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

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

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

1. Outline

  1. 起點 · Reactivity in Vue 3 - How does it work?
    從「純 JavaScript 不會自己重算」這個反例出發,一層一層長出 effect、dep、depsMap、targetMap,把 Vue 3 響應式的「儲存端」完整做出來;自動重跑留給下一課。

2. YouTuber 的思維推導

Reactivity in Vue 3 - How does it work?

Vue Mastery · 9m47s · 字幕 en · vision=on (整支是螢幕錄影:作者一邊寫程式一邊指著編輯器與圖解(「Here you can see」「Let's look back at a diagram」「let me show you what I mean」),dep / depsMap / targetMap 三層結構全靠圖說明,不看畫面讀不懂。) · YouTube

預估 vs 實際耗時
階段預估實際
fetch 6s 2s
segment 50s 3m00s
shot 31s 9s
analyze 3m45s 8m57s
render 5s –

作者的起點是一個落差:你天天在用 Vue 的響應式,卻說不出 Vue 憑什麼知道 price 一改、模板裡三個地方就要跟著變。他先用一個最小的 Vue app 把這個落差變成一句可回答的問題,再把 Vue 整個拿掉、換成十行純 JavaScript,證明「改了來源、結果不會重算」是語言的預設行為——響應式不是天上掉下來的,是有人做出來的。確認要自己做之後,他把大問題切成兩塊拼圖:先解決「怎麼把一段計算存起來」,再解決「怎麼在值改變時自動重跑」,而這支影片只做第一塊。

接著他用一條非常固定的節奏往上長:每次先讓現有方案撞牆,再加剛好一層結構補上。把計算包成匿名函式存進 effect,需要一個放它的地方 → dep(Set,天生去重);一個 Set 只服務一個值,而物件有很多屬性 → 上面加一張以屬性名為 key 的 depsMap;一張 depsMap 只屬於一個物件,而 app 裡有很多物件 → 再上面加一張以物件本身為 key 的 targetMap(WeakMap)。每加一層,他都先畫圖再翻成程式碼,而且刻意沿用 Vue 3 原始碼裡的名字(effect、track、trigger、dep、depsMap、targetMap),讓讀者學到的不只是一個玩具,而是能直接對照原始碼的心智模型。

最後他回到同一張三層圖收束,並誠實指出成品的缺口:track 和 trigger 目前都得手動呼叫,自動化是下一課的事。

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

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

  1. 為什麼要懂 Vue 3 響應式 → 動機講完了,但「Vue 3 到底怎麼做」還一個字都沒說。作者留下的線索是:要看懂新做法,得先把問題問對——Vue 究竟是憑什麼知道有哪些地方需要更新?下一步要用一個最小的 app 把這個問題變成看得見的東西。
  2. Vue 怎麼知道要更新畫面 → 問題問出來了:Vue 知道該更新哪三個地方。但作者還沒說這件事為什麼需要特別做——也許 JavaScript 本來就會幫你重算?下一段要把 Vue 整個拿掉,用純 JavaScript 驗證這個假設。
  3. 純 JavaScript 沒有響應式 → 確認了語言不會幫忙,唯一的出路是把「算 total 的那段程式碼」留起來以便重跑。但留在哪裡、由誰在什麼時候重跑,都還是空白——下一段作者要正式宣告要自己造這個東西,並把大問題切成第一個做得動的小問題。
  4. 從零打造響應式引擎 → 子問題定下來了:把 total 的計算存起來。要做到這件事至少需要三樣東西——一段可以被存的程式碼、一個存放處、一個把存起來的東西重跑的動作。下一段作者要給這三樣東西名字,而且是 Vue 原始碼裡的名字。
  5. effect、track、trigger 三個名字 → 三個名字都到位了,但畫面右邊的 storage 到目前為止還只是一個藍色方框——它到底是陣列、物件還是別的東西?下一段要把這個方框換成真正的資料結構,而選哪一種是有理由的。
  6. 用 Set 當儲存空間 dep → 現在只有一個全域的 dep,意思是 price 和 quantity 共用同一票 effect:改 price 會連只用到 quantity 的程式碼一起重跑。但真實物件有很多屬性,下一段要讓每個屬性各自擁有一個 dep。
  7. 每個屬性各自一個 dep → 圖已經畫好:depsMap 的每個 key 是屬性名、value 是一個裝 effect 的 Set。但 track 和 trigger 目前還是上一版、只認得那個唯一的 dep。下一段要把這張圖翻成程式碼。
  8. 把 depsMap 寫成程式碼 → 同一個物件的不同屬性現在可以各自追蹤了。但整張 depsMap 是個全域變數、暗地裡只屬於 product:一旦再來一個 user 物件,它的屬性依賴就無處可放。下一段要在 depsMap 上面再加一層。
  9. 多個物件與 targetMap → 三層結構的圖完整了,WeakMap 的用法也示範過了。但 track 和 trigger 目前只收 key、不認得 target,多出來的那一層還沒進到程式碼裡——下一段要補上最後一層實作。
  10. 把 targetMap 寫成程式碼 → 引擎的儲存端到此完工,也親眼看到 total 從 10 變成 15。但每次都要手動 track、手動 trigger。最後一段要把三層結構收成三句話,並點名還缺的那一半是什麼。
  11. 三層結構回顧與下一課

3. 逐段說明

Reactivity in Vue 3 - How does it work?

1. 為什麼要懂 Vue 3 響應式 0:00–1:00

作者開場說明學這門課的四個理由:理解 Vue 3 底層設計、看得懂 stack trace 好除錯、reactivity 在 Vue 3 是獨立模組可單獨拿來用、想貢獻 Vue 原始碼時看得懂。並強調 Vue 3 的做法與 Vue 2 完全不同,是全新內容。

承上 前情提要裡「你已經會用 Vue、寫過 template 和 computed,但沒打開過引擎蓋」這個背景正是作者要用的槓桿:他不需要教你 Vue 怎麼寫,只需要指出你每天依賴、卻從沒問過原理的那件事。

推理因為觀眾多半停在「會用」的層次,所以作者一開場不講技術,先講四個回報:看懂底層設計、除錯時讀得懂 stack trace、把 reactivity 當獨立模組單獨使用、以及看得懂原始碼好貢獻。接著他補上一句最關鍵的定位——Vue 3 的做法跟 Vue 2 完全不同,就算你看過他以前的 reactivity 課程,這裡也是全新內容。

AI 補充「完全不同」值得說得更具體一點,因為它決定了後面每一步為什麼要這樣走。Vue 2 是在元件初始化時走一遍 data,把每個屬性用 Object.defineProperty 改寫成 getter/setter,所以它一開始就得知道有哪些屬性——事後新增的屬性偵測不到,陣列索引賦值也偵測不到。Vue 3 改成用 Proxy 攔截整個物件的讀寫,屬性是動態的也攔得到。而第三個理由(reactivity 是獨立模組)在架構上是真的:Vue 3 把它拆成一個叫 @vue/reactivity 的套件,不裝 Vue 也能單獨 npm 安裝來用。這支影片要重建的,就是這個套件內部真正的資料結構。

reactivity

響應式

資料改變時,所有用到這份資料的地方自動跟著更新的機制。

響應式不是 JavaScript 內建的能力,而是框架自己實作出來的:它必須在「有人讀取某個值」時記下是誰讀的,並在「有人寫入某個值」時把記下的那些人重新跑一次。Vue、Svelte、SolidJS、MobX 的差別幾乎都在這兩個動作的實作方式上。常見誤解是把它當成「雙向綁定」——雙向綁定只是響應式的一種應用,響應式本身是單向的資料流。這支影片只處理「記下是誰讀的」這一半。

相關術語: Proxy (Vue 3 的實作基礎)、Object.defineProperty (Vue 2 的舊實作)

出處:第 1 段「為什麼要懂 Vue 3 響應式」

stack trace

呼叫堆疊追蹤

程式出錯時列出的函式呼叫層層路徑,用來回推錯誤是從哪裡一路呼叫過來的。

Vue 出錯時堆疊裡常常夾著框架內部的函式名。若不知道那些名字代表什麼,這段訊息就只是雜訊;認得 effect、track、trigger 之後,你可以直接從堆疊看出「錯誤發生在某個 effect 被重跑的時候」。這正是作者把課程用字和原始碼對齊的實際好處之一。

相關術語: effect (堆疊中常見的名字)

出處:第 1 段「為什麼要懂 Vue 3 響應式」

@vue/reactivity

Vue 響應式獨立套件

Vue 3 把響應式系統抽成的獨立 npm 套件,可以不搭配 Vue 元件單獨使用。

它匯出 reactive、ref、computed、effect 等 API,不依賴任何渲染邏輯,所以可以拿去驅動 canvas、Node.js 程式甚至別的 UI 框架。這種拆分也是 Vue 3 與 Vue 2 架構上最大的差異之一:Vue 2 的響應式和元件系統纏在一起,拆不出來。本支影片手工重建的三層資料結構,就住在這個套件裡。

相關術語: reactivity (它實作的東西)

出處:第 1 段「為什麼要懂 Vue 3 響應式」

Object.defineProperty

定義物件屬性

JavaScript 用來替物件的單一屬性安裝自訂 getter / setter 的方法。

Vue 2 靠它把 data 裡的每個屬性換成 getter/setter,藉此攔截讀寫。限制在於它一次只能處理一個「已存在」的屬性:初始化之後才加上去的屬性攔不到(所以 Vue 2 需要 Vue.set),用索引直接改陣列元素也攔不到。這些限制正是 Vue 3 換掉它的理由。

相關術語: Proxy (Vue 3 的替代品)

出處:第 1 段「為什麼要懂 Vue 3 響應式」

Proxy

代理物件

ES6 的內建物件,包住一個目標物件並攔截對它的所有讀取、寫入等操作。

Proxy 攔截的是「整個物件」而不是「某個屬性」,所以動態新增的屬性、陣列索引賦值都攔得到。它的攔截函式(handler)第一個參數就叫 target,也就是被代理的原始物件。這支影片還不會用到 Proxy,但它是把手動呼叫的 track / trigger 變成自動的關鍵,也是後面那個資料結構為什麼叫 targetMap 的線索。

相關術語: Object.defineProperty (被它取代)、reactivity (實作於)

出處:第 1 段「為什麼要懂 Vue 3 響應式」

留給下一段 動機講完了,但「Vue 3 到底怎麼做」還一個字都沒說。作者留下的線索是:要看懂新做法,得先把問題問對——Vue 究竟是憑什麼知道有哪些地方需要更新?下一步要用一個最小的 app 把這個問題變成看得見的東西。

2. Vue 怎麼知道要更新畫面 1:00–1:30

從一個最簡單的 Vue app 出發:模板裡用到 price、也用到依賴 price 的 computed 屬性。當 price 改變,Vue 就自動更新模板裡三個用到它的地方。作者把整門課的問題定成一句話:Vue 到底怎麼知道該更新哪些東西?

同時看到 data 裡的 price 與模板中三個用到它的位置,才知道「一個值改變、三處要更新」的依賴關係長什麼樣子。
1:10 · 同時看到 data 裡的 price 與模板中三個用到它的位置,才知道「一個值改變、三處要更新」的依賴關係長什麼樣子。
承上 承上:上一段留下「Vue 究竟憑什麼知道要更新什麼」這個還沒問清楚的問題,這一段用一個最小的 Vue app 把它變成畫面上數得出來的三個位置。

推理因為上一段只給了學習動機、沒給問題,所以作者接著挑一個小到不能再小的例子:data 裡只有 price 和 quantity,模板裡用了 price、用了 price * quantity,還用了一個依賴兩者的 computed 屬性。他改一次 price,指著模板說「這裡、這裡,還有這個 computed 又更新了模板這裡」,把整門課的問題定成一句話——Vue 到底怎麼知道該更新哪些東西?

AI 補充畫面上這個例子有兩件事值得補。第一,作者指的三處其實不是同一種:模板裡兩處是直接依賴 price,第三處是先依賴 computed 屬性 totalPriceWithTax、再由它依賴 price——依賴是會串成鏈的,中間節點自己也要被追蹤。這條鏈之所以重要,是因為它排除了「掃描整個模板重畫一次」這種土法:Vue 更新的粒度是「用到這個值的那幾個地方」,不是整頁。第二,投影片上的程式碼寫的是 var vm = new Vue({ el: '#app', data, computed }),那是 Vue 2 的寫法;機制講的是 Vue 3,但範例語法沒跟著換。看的時候把注意力放在依賴關係上,不要照抄這段初始化程式碼。

術語:computed property

template

模板

描述畫面長相的那段 HTML,裡面用雙大括號等語法插入資料。

模板不是死的字串,Vue 會把它編譯成一個渲染函式;這個函式執行時會去讀取它用到的每個資料屬性。「執行時會去讀取」這件事是整個響應式系統的入口——正因為讀取這個動作可以被攔截,Vue 才有機會記下「這段畫面用到了 price」。所以模板在本質上就是後面要講的 effect 的一種。

相關術語: computed property (常被它取用)、dependency (產生)

出處:第 2 段「Vue 怎麼知道要更新畫面」

computed property

計算屬性

由其他資料算出來、並在來源改變時自動重算的衍生值。

它同時是消費者也是生產者:對 price 來說它是依賴者,對模板來說它是被依賴者,所以依賴會串成鏈。computed 和普通函式的差別在於它會快取結果,來源沒變就不重算。這個「只有來源變了才重算」的行為,正是這支影片要手工做出來的能力。

相關術語: template (被它取用)、dependency (串成鏈)

出處:第 2 段「Vue 怎麼知道要更新畫面」

dependency

依賴

某段程式碼在執行時讀到了某個值,於是那個值改變時這段程式碼必須重跑的關係。

注意方向:依賴是「程式碼依賴值」,所以框架要記的是「值 → 有哪些程式碼在等它」這張反向對照表。手動維護這張表就是本課要做的事。一個常見誤解是以為 Vue 靠比對前後資料(diff)來決定更新誰——那是另一種策略;Vue 走的是事先登記的路線,改動時直接查表,不必掃描。

相關術語: computed property (會串成鏈)、reactivity (組成)

出處:第 2 段「Vue 怎麼知道要更新畫面」

確定錯誤/已過時 1:00
「And it starts by taking a look at a simple Vue app.」
這裡搭配的投影片用的是 Vue 2 的建立方式 var vm = new Vue({ el: '#app', ... }),但這是一門 Vue 3 課程。Vue 3 已移除全域的 new Vue() 建構式,改成 createApp({ ... }).mount('#app');照著螢幕寫在 Vue 3 專案裡會直接壞掉。這只影響範例語法,不影響後面要講的響應式機制。
依據: Vue 3 遷移指南 Global API: createApp(new Vue 已移除)
留給下一段 問題問出來了:Vue 知道該更新哪三個地方。但作者還沒說這件事為什麼需要特別做——也許 JavaScript 本來就會幫你重算?下一段要把 Vue 整個拿掉,用純 JavaScript 驗證這個假設。

3. 純 JavaScript 沒有響應式 1:30–1:50

作者把同樣的邏輯改寫成純 JavaScript:price 乘 quantity 得到 total,印出來是 10;接著改掉 price,再印 total 仍然是 10。JavaScript 不會自己重算,這證明響應式不是語言本來就有的能力,得自己做出來。

看到 console 裡改了 price 之後 total 仍是 10,才能直觀感受「不會自動重算」這件事。
1:44 · 看到 console 裡改了 price 之後 total 仍是 10,才能直觀感受「不會自動重算」這件事。
承上 承上:上一段留下「這件事憑什麼需要特別做」的疑問,這一段把 Vue 拿掉、只留純 JavaScript,直接驗證語言本身會不會自動重算。

推理因為上一段的問題必須先確認它真的是個問題,所以作者接著寫下最短的反例:price = 5、quantity = 2、total = price * quantity,印出來是 10;接著把 price 改成 20,再印一次 total,還是 10。他下的結論很短——它不會被更新,這裡沒有任何響應式。

AI 補充這個反例的關鍵不在「JavaScript 很笨」,而在賦值的語意。let total = price * quantity 這一行做的是:現在就把 price * quantity 算出來,把算出來的那個數字 10 放進 total。total 存的是一個結果,不是「price 乘 quantity」這條關係;乘法在賦值那一瞬間就用完了,之後 price 變成什麼都與 total 無關。看清楚這點,解法的形狀就跟著確定了:既然關係本身沒被保存,那就得想辦法把「算 total 的那段程式碼」原封不動地留下來,等來源變了再執行一次。這也解釋了為什麼後面第一件事是把計算包成函式——函式是 JavaScript 裡把「一段還沒發生的計算」存起來的唯一方式。

術語:eager evaluation

eager evaluation

及早求值

運算式在被寫下的那一刻就立即算完,變數存的是結果值而不是算式。

JavaScript 的賦值一律是及早求值,所以 total 拿到的是數字 10 而不是一條乘法規則。它的相反是 lazy evaluation(延遲求值):把運算包成可呼叫的東西,需要時才算——這正是把計算包進函式的意義。響應式系統其實是在及早求值的語言上,用函式手工模擬出「這條算式一直有效」的錯覺:值先算好一次,來源變了再重算一次。

相關術語: reactivity (要繞過的限制)

出處:第 3 段「純 JavaScript 沒有響應式」

留給下一段 確認了語言不會幫忙,唯一的出路是把「算 total 的那段程式碼」留起來以便重跑。但留在哪裡、由誰在什麼時候重跑,都還是空白——下一段作者要正式宣告要自己造這個東西,並把大問題切成第一個做得動的小問題。

4. 從零打造響應式引擎 1:50–2:12

作者宣告接下來兩課要從零打造一個響應式引擎,而且做法會和 Vue 3 內部完全一樣。第一個要回答的子問題是:怎麼把 total 的計算「存起來」,讓 price 或 quantity 改變時可以再跑一次?

承上 承上:上一段推出「得把計算留起來以便重跑」,但沒說留在哪裡;這一段作者宣告要親手造一個引擎,並把這個空白正式立成第一個要解的子問題。

推理因為上一段證明了 JavaScript 不會自己重算,所以作者接著宣告:這門課的前兩堂要從零打造一個響應式引擎,而且會用和 Vue 3 一模一樣的方式做。他馬上把大問題收窄成一句可執行的提問——我們要怎麼把 total 的計算存起來,好讓 price 或 quantity 更新的時候可以再跑一次?

AI 補充這裡的切法值得停一下。「自動更新」其實藏了兩個獨立的能力:一是把該重跑的程式碼存下來(儲存端),二是在值改變的當下自動去跑它(觸發端)。作者只把第一個立成問題,第二個留給下一課;這也是為什麼接下來所有的 track 和 trigger 都得手動呼叫——不是他忘了,而是自動化被刻意排在後面。先確定「存什麼、存在哪」,自動化才有東西可以觸發;順序反過來會無從下手。另外「從零打造」在這裡不是噱頭:因為結構和名字都跟原始碼對齊,做完之後你看 Vue 原始碼會發現變數名一個一個對得上。

reactivity engine

響應式引擎

負責記錄「誰用了哪個值」並在值改變時重跑對應程式碼的那一小組資料結構與函式。

拆開來看它只有兩個動作和三層資料結構,程式碼量比想像中小得多——這支影片手工寫出來的版本不到二十行。之所以看起來神祕,是因為在真實框架裡它被 Proxy 包住、從不需要你直接呼叫。把它單獨拆出來手寫一次,是理解 Vue 最短的路。

相關術語: @vue/reactivity (對應的真實套件)

出處:第 4 段「從零打造響應式引擎」

見仁見智 1:57
「in the same exact way, that Vue 3 does reactivity.」
「一模一樣」要打點折。三層結構(targetMap → depsMap → dep)與命名確實和當時 Vue 3.0–3.3 的原始碼一致,但課程版本省略了 activeEffect 堆疊、巢狀 effect、舊依賴清除、排程佇列、ref 與 computed 等大量真實邏輯。而且 Vue 3.4(2023-12)與 3.5(2024-09)兩次重寫 reactivity 之後,內部細節已經和影片不同,「一模一樣」在今天更接近「概念上一致」。
依據: Vue 3.4 / 3.5 reactivity 重寫,packages/reactivity/src/dep.ts
留給下一段 子問題定下來了:把 total 的計算存起來。要做到這件事至少需要三樣東西——一段可以被存的程式碼、一個存放處、一個把存起來的東西重跑的動作。下一段作者要給這三樣東西名字,而且是 Vue 原始碼裡的名字。

5. effect、track、trigger 三個名字 2:12–3:06

作者把 total 的計算包成匿名函式存進變數 effect,用 track 把它存起來,先跑一次算出 total,之後再呼叫 trigger 把存起來的程式碼全部重跑。他特別點名 effect、track、trigger 就是 Vue 3 原始碼裡用的名字。

這張畫面上同時有 effect、track()、effect()、trigger() 的呼叫順序,是整個引擎的骨架。
2:55 · 這張畫面上同時有 effect、track()、effect()、trigger() 的呼叫順序,是整個引擎的骨架。
承上 承上:上一段留下「存什麼、存在哪、誰來重跑」這三個待命名的空位,這一段把它們一次填上,而且刻意用 Vue 3 原始碼裡的名字。

推理因為上一段把問題縮成「怎麼把計算存起來」,所以作者接著重寫一次程式:把 total = price * quantity 包進一個匿名函式,存進變數 effect——這就是「我們想存起來的程式碼」。然後他排出呼叫順序:先 track()(把 effect 存進 storage),再 effect()(第一次算出 total),之後某個時間點呼叫 trigger()(把 storage 裡所有程式碼跑一遍)。最後他點名這三個字和 Vue 3 reactivity 原始碼裡看到的完全一樣。

AI 補充為什麼一定要「包成函式」,值得說白:total = price * quantity 是一個敘述,寫下去就執行完了,你沒辦法把一個已經執行完的敘述交給別人保管;但 () => { total = price * quantity } 是一個值,可以放進變數、塞進集合、之後再呼叫。函式是 JavaScript 裡唯一能把「還沒發生的計算」當成資料傳來傳去的東西,這是整個引擎能成立的語言前提。另外看畫面上的順序:track() 在 effect() 之前,是「先登記、再執行」。真實的 Vue 反過來——它在執行 effect 的過程中,因為讀到了 price 才順手登記。這裡先用手動呼叫代替那個自動登記的時機,是刻意的簡化,也是後面唯一還沒被解掉的人工成分。

effect

副作用函式

一段被包成函式、需要在它讀到的值改變時重新執行的程式碼。

名字來自 side effect:這個函式的價值不在回傳值,而在它執行時對外界造成的改變(更新 total、重繪畫面)。在真實的 Vue 裡,元件的渲染函式就是一個 effect,每個 watch 和 computed 內部也各自包著一個。因此「值改變 → 畫面更新」在框架內部其實是「值改變 → 對應的 effect 被重跑」。

相關術語: anonymous function (用它包裝)、track (被它存起來)

出處:第 5 段「effect、track、trigger 三個名字」

track

追蹤

把目前這個 effect 登記進某個值的儲存空間,表示這個值改變時要重跑它。

track 是響應式的「讀取」側動作:真實 Vue 在 Proxy 的 get 攔截器裡呼叫它,所以任何一次屬性讀取都會順手完成登記。本課因為還沒有攔截機制,只能手動在對的時機呼叫。名字裡的「追蹤」指的是追蹤依賴關係,不是追蹤值本身。

相關術語: trigger (相反動作)、dependency (登記的內容)

出處:第 5 段「effect、track、trigger 三個名字」

trigger

觸發

把某個值儲存空間裡登記過的 effect 全部重新執行一次。

trigger 是「寫入」側動作,真實 Vue 在 Proxy 的 set 攔截器裡呼叫它。track 與 trigger 是一對:沒有 track 過的值,trigger 時查表查不到任何人,什麼都不會發生。實務上 trigger 不會立刻同步執行所有 effect,而是丟進排程佇列去重後在下一個 tick 一起跑,避免同一輪改動觸發多次重繪。

相關術語: track (相反動作)、effect (重跑的對象)

出處:第 5 段「effect、track、trigger 三個名字」

anonymous function

匿名函式

沒有名字、直接當成值賦給變數或傳給別人的函式。

這裡用它的理由不是省字,而是要強調這段程式碼是一份「資料」:它會被存進集合、被別人在未知的時間點取出來執行,呼叫它的人根本不需要知道它叫什麼。順帶一提,因為之後要靠函式的同一性來去重,同一個 effect 必須是同一個函式參考——每次現寫一個新的匿名函式,就會被當成不同的 effect。

相關術語: effect (它的用途)、closure (常一起出現)

出處:第 5 段「effect、track、trigger 三個名字」

留給下一段 三個名字都到位了,但畫面右邊的 storage 到目前為止還只是一個藍色方框——它到底是陣列、物件還是別的東西?下一段要把這個方框換成真正的資料結構,而選哪一種是有理由的。

6. 用 Set 當儲存空間 dep 3:06–3:57

作者用一個叫 dep 的 Set 來存 effect,dep 是 dependency 的縮寫;選 Set 是因為它不允許重複值,同一個 effect 加兩次也只會有一份。trigger 就是走訪 dep 裡每個 effect 並執行。實測:改了 quantity 後 total 仍是 10,跑 trigger 才變成 15。

看到 dep = new Set()、track 裡 dep.add(effect)、trigger 裡 dep.forEach 的完整寫法,才知道「儲存空間」具體是什麼。
3:25 · 看到 dep = new Set()、track 裡 dep.add(effect)、trigger 裡 dep.forEach 的完整寫法,才知道「儲存空間」具體是什麼。
console 上 total 從 10 變成 15 的那一刻,是這一段推理成立的證據。
3:53 · console 上 total 從 10 變成 15 的那一刻,是這一段推理成立的證據。
承上 承上:上一段的 storage 還只是圖上的一個方框,這一段把它落地成具體的資料結構,並說明為什麼選這一種。

推理因為上一段留下「storage 到底是什麼」沒答,所以作者接著先把 effect 改寫成箭頭函式讓程式短一點,再宣告 let dep = new Set()——dep 是 dependency 的縮寫。track 的內容就一行 dep.add(effect),trigger 則是 dep.forEach(effect => effect())。他特別交代選 Set 的理由:Set 不允許重複值,同一個 effect 加兩次也只會留一份。最後在 console 實測:total 是 10,把 quantity 改成 3,total 還是 10,跑一次 trigger(),total 就變成 15。

AI 補充去重為什麼值得為它挑一種資料結構?因為在真實情境裡重複登記是常態而非例外:模板裡如果三個地方都讀到 price,同一個渲染 effect 就會被 track 三次。用陣列存,price 改一次就會重繪三次;如果那個 effect 又寫回了自己讀過的值,還可能滾成無窮迴圈。Set 把去重變成資料結構的天然性質,呼叫端不必先檢查存不存在。但要注意 Set 判定重複用的是同一性(===):只有「同一個函式參考」才算重複,兩個內容一模一樣但分別建立的箭頭函式會被當成兩個 effect。另外,這個 demo 之所以跑得動,是因為 effect 是一個閉包——它抓的是外層的 price 和 quantity 變數本身,而不是當初的值,所以重跑時讀到的自然是新值。前一段提到的「值不會自己更新」在這裡被閉包補上了另一半:關係被保存在函式裡,只差有人按下重播。

dep

依賴集合

存放「這個值改變時要重跑的所有 effect」的那個集合,名字是 dependency 的縮寫。

dep 是這套系統裡最小的單位:一個值一個 dep,dep 裡是一票等著被重跑的 effect。它容易被誤讀成「這段程式碼依賴了什麼」,方向其實相反——它記的是「有誰依賴我」。在 Vue 原始碼裡你會直接看到這個變數名,只是外面還包了兩層查找結構。

相關術語: effect (裡面裝的東西)、Set (採用的型別)

出處:第 6 段「用 Set 當儲存空間 dep」

Set

集合

ES6 內建的容器,成員不重複,加入重複值會被自動忽略。

和陣列的差別有三:成員唯一、判斷存在是常數時間、沒有索引順序的語意。這裡看中的是第一點——去重是資料結構自己保證的,不需要在 add 之前先寫一行 includes 檢查。它判斷重複用的是 SameValueZero(實務上等同 ===),所以物件與函式比的是參考而不是內容。

相關術語: dep (被用來實作)、Map (同期的姊妹結構)

出處:第 6 段「用 Set 當儲存空間 dep」

arrow function

箭頭函式

用 () => {} 寫成的簡短函式語法,沒有自己的 this。

這裡換成箭頭函式純粹是為了讓投影片一行放得下,語意和原本的匿名函式相同。真正有差別的地方在 this:箭頭函式沿用外層的 this,所以在 Vue 元件裡寫 effect 時不會意外綁到別的物件上。也因為每寫一次就產生一個新的函式物件,把箭頭函式直接寫在 track 的參數裡會讓 Set 的去重失效。

相關術語: anonymous function (一種寫法)、closure (同樣會形成)

出處:第 6 段「用 Set 當儲存空間 dep」

closure

閉包

函式連同它出生時所在的外層變數環境一起被保存下來的現象。

閉包抓住的是變數本身,不是變數當時的值——這是這個 demo 能成立的關鍵:effect 被存進 Set、擱置一段時間之後再跑,讀到的仍是最新的 price 和 quantity。若當初存的是算好的數字,重跑就毫無意義。可以說響應式系統賣的是「延後執行」,而閉包是讓延後執行仍然正確的那個語言特性。

相關術語: eager evaluation (繞過它的手段)、effect (使它可重跑)

出處:第 6 段「用 Set 當儲存空間 dep」

確定錯誤/已過時 3:13
「we'll use a dep variable which stands for dependency, and it's a new set.」
在影片當時的 Vue 3.0–3.3 裡,dep 確實是一個 Set<ReactiveEffect>;但這一點今天已經過時。Vue 3.4 把 dep 改成 Map(effect → trackId 版本號)以支援依賴去重與版本比對,Vue 3.5 再把它改寫成 Dep 類別 + 雙向鏈結串列(Link 節點),Set 已經不存在了。targetMap 與 depsMap 這兩層仍在,所以本課的三層心智模型依然適用,只有最內層的容器換過。
依據: Vue 3.4(2023-12)、3.5(2024-09)reactivity 重寫;packages/reactivity/src/dep.ts
留給下一段 現在只有一個全域的 dep,意思是 price 和 quantity 共用同一票 effect:改 price 會連只用到 quantity 的程式碼一起重跑。但真實物件有很多屬性,下一段要讓每個屬性各自擁有一個 dep。

7. 每個屬性各自一個 dep 3:57–5:02

真實的物件有很多屬性,price 和 quantity 各自需要一個 dep。作者把兩個值包成 product 物件,並提出 depsMap:一個 Map,key 是屬性名稱(quantity、price),value 是該屬性的 dep,也就是一組要重跑的 effect。

看到 price、quantity 被包進 product 物件,才理解「每個屬性各自一個 dep」的需求從哪來。
4:10 · 看到 price、quantity 被包進 product 物件,才理解「每個屬性各自一個 dep」的需求從哪來。
depsMap 的圖解:左邊 key 是屬性名、右邊 value 是一個裝 effect 的 Set,這張圖是後面兩層結構的基礎。
4:50 · depsMap 的圖解:左邊 key 是屬性名、右邊 value 是一個裝 effect 的 Set,這張圖是後面兩層結構的基礎。
承上 承上:上一段只有一個共用的 dep,會讓不相干的 effect 一起重跑;這一段要替每個屬性各配一個 dep,並解決「這些 dep 之後怎麼找回來」。

推理因為上一段的 dep 是全域唯一的,所以作者接著把場景升級:先把 price 和 quantity 收進一個 product 物件,指出每個屬性都需要自己的一組 effect,然後問「要怎麼替每個屬性各存一個 dep」。他的答案是再包一層——depsMap,一個 Map,key 用物件的屬性名稱(quantity、price),value 就是各自的那個 dep。他也順手把 dep 的定義說死:一組在值改變時應該重跑的 effect,型別是 Set。

AI 補充為什麼 key 是屬性「名稱」而不是屬性的值?因為值會變,而依賴關係要在值變前後都指向同一個位置——用名稱當 key,改 quantity 時才找得回當初登記在 quantity 底下的那票 effect。這也解釋了為什麼 Vue 追蹤的粒度是「物件的某個 key」而不是「某個變數」。至於為什麼用 Map 而不是普通物件:Map 的 key 可以是任意型別(下一步就要用物件當 key)、有 size、迭代順序穩定,而且不會和 Object.prototype 上的名字撞在一起(屬性剛好叫 constructor 或 toString 時,普通物件會出事)。還有一個容易滑過去的改動:圖上的 effect 已經從 price * quantity 悄悄變成 product.price * product.quantity。它現在讀的是「某個物件的某個屬性」,這正是後續能攔截到「你讀了誰的哪一個 key」的前提;如果還是裸變數,再多層 Map 也沒有東西可以當 key。

depsMap

屬性依賴表

一個 Map,替某個物件的每一個屬性名稱各存一個 dep。

它是三層結構的中間層:往下每個 value 是一個 dep,往上整張表屬於某一個響應式物件。名字是 deps(複數的 dep)加 Map,讀作「很多 dep 的對照表」。這一層的存在直接決定了 Vue 的更新精度——因為 dep 是按 key 分開的,改 quantity 不會驚動只讀了 price 的畫面。

相關術語: dep (裡面存的值)、Map (採用的型別)

出處:第 7 段「每個屬性各自一個 dep」

Map

映射表

ES6 內建的鍵值對容器,key 可以是任何型別,並保持插入順序。

相對於用普通物件當字典,Map 的 key 不會被強制轉成字串、不會和原型上的名字衝突、有 size 屬性、也能直接迭代。這裡先用字串當 key(屬性名),但選 Map 而不是物件是有伏筆的:下一層要拿「物件本身」當 key,普通物件做不到,Map 家族才做得到。

相關術語: Set (同期的姊妹結構)、depsMap (實作於)

出處:第 7 段「每個屬性各自一個 dep」

留給下一段 圖已經畫好:depsMap 的每個 key 是屬性名、value 是一個裝 effect 的 Set。但 track 和 trigger 目前還是上一版、只認得那個唯一的 dep。下一段要把這張圖翻成程式碼。

8. 把 depsMap 寫成程式碼 5:02–6:07

track 先用 key 去 depsMap 拿 dep,沒有就新建一個 Set 存進 map,再把 effect 加進去;trigger 先用 key 拿 dep,存在才走訪執行。回到原本的程式:track('quantity') 存下 effect,改 quantity 後呼叫 trigger('quantity'),total 從 10 變 15。

track / trigger 兩個函式改用 depsMap 的完整程式碼,get、沒有就 new Set、set 回去的順序看一次就懂。
5:30 · track / trigger 兩個函式改用 depsMap 的完整程式碼,get、沒有就 new Set、set 回去的順序看一次就懂。
承上 承上:上一段停在 depsMap 的示意圖,而 track / trigger 還是只認得單一 dep 的舊版;這一段把圖翻成可以執行的程式碼。

推理因為上一段只給了結構圖,所以作者接著逐行實作:track(key) 先 depsMap.get(key) 拿這個屬性的 dep,如果還沒有就建一個新的 Set 用這個 key 存回 map,再 dep.add(effect);trigger(key) 則先拿這個 key 的 dep,存在才走訪每個 effect 執行。回到主程式:呼叫 track('quantity') 存下 effect、跑一次 effect 得到 total 10,把 product.quantity 改成 3 之後 total 仍是 10,直到呼叫 trigger('quantity'),total 才變成 15。

AI 補充有兩個寫法值得拆開看。第一是 depsMap.set(key, (dep = new Set())):JavaScript 的賦值本身是一個運算式、會回傳被賦的值,所以這一行同時做完三件事——建立新 Set、讓區域變數 dep 指向它、把它存進 map。這種「拿不到就現場建一個並存回去」的模式在下一層還會再出現一次,記住它的形狀,看完整版程式碼時會輕鬆很多。第二是 trigger 裡的 if (dep):一個從來沒被 track 過的屬性,depsMap.get 會回傳 undefined,少了這個檢查就會對 undefined 呼叫 forEach 而爆錯。換句話說,「這個屬性沒有任何人在等」是正常狀態,不是錯誤。同時要注意這一版最大的人工成分還在原地:track 的參數是手打的字串 'quantity',effect 裡明明也讀了 product.price,卻沒人替 price 登記——改 price 依然不會有任何反應。

術語:lazy initialization

lazy initialization

延遲初始化

第一次真的需要用到時才建立那個物件,而不是預先全部建好。

這裡的 if (!dep) { ...new Set()... } 就是它:沒人追蹤過的屬性不會平白多出一個空 Set。好處除了省記憶體,更重要的是不必事先知道物件有哪些屬性——這正好呼應 Vue 2 必須在初始化時走遍所有屬性的限制。實作上它幾乎總是長成「get → 沒有就建 → set 回去 → 使用」這個四步驟,看到這個形狀就可以直接讀懂意圖。

相關術語: depsMap (用於填充)、Object.defineProperty (對照的舊做法)

出處:第 8 段「把 depsMap 寫成程式碼」

留給下一段 同一個物件的不同屬性現在可以各自追蹤了。但整張 depsMap 是個全域變數、暗地裡只屬於 product:一旦再來一個 user 物件,它的屬性依賴就無處可放。下一段要在 depsMap 上面再加一層。

9. 多個物件與 targetMap 6:07–7:34

如果不只 product,還有 user 物件呢?需要再上面一層 Map,key 是響應式物件本身。Vue 3 把它叫 targetMap,型別是 WeakMap——一種 key 必須是物件的 Map。作者用 console 示範 WeakMap 的 set 與 get,並先賣個關子:為什麼叫 target,下一課才會說。

targetMap → depsMap → dep 三層結構的圖解,這張圖是整支影片的核心,光聽講很難在腦中拼出來。
6:40 · targetMap → depsMap → dep 三層結構的圖解,這張圖是整支影片的核心,光聽講很難在腦中拼出來。
console 裡用物件當 key 做 set / get 的實際示範,才會相信 WeakMap 的 key 真的是物件而不是字串。
7:15 · console 裡用物件當 key 做 set / get 的實際示範,才會相信 WeakMap 的 key 真的是物件而不是字串。
承上 承上:上一段的 depsMap 實際上綁死在單一物件上,多一個物件就沒地方放;這一段就是要在它上面補上以物件為 key 的那一層。

推理因為上一段的結構只服務得了一個物件,所以作者接著問「如果有多個響應式物件呢」,並在圖上多畫一個 user 物件。他的解法延續同一個套路:再往上放一層 map,key 以某種方式指向響應式物件本身(product 或 user)。他指出 Vue 3 裡這一層叫 target map,型別是 weak map,並定義 weak map 就是 key 必須是物件的 map,接著在 console 現場示範:new WeakMap()、set(product, 'example code to test')、get(product) 拿回同一個字串。最後他自曝也好奇「為什麼叫 target」,說下一課就會清楚。

AI 補充作者對 WeakMap 的定義只講了「key 是物件」,這剛好漏掉了名字裡最重要的那個字。weak 指的是這張表對 key 只持有弱引用:如果程式其他地方都不再引用 product,垃圾回收就可以把它連同這裡登記的整張 depsMap 一起收掉。這才是 Vue 非用 WeakMap 不可的理由——targetMap 是一個全域、永遠活著的變數,如果用普通 Map,每個曾經變成響應式的物件都會被它永久扣住,元件卸載一千次就洩漏一千份,這是教科書等級的記憶體洩漏。順帶補上作者賣關子的那個答案:target 這個名字來自 Proxy(見第 1 段)攔截函式的第一個參數,也就是「被代理的原始物件」。等自動追蹤上場時,track 拿到的 target 正是從 Proxy 傳進來的那個物件,名字就自然對上了。

targetMap

目標物件依賴表

三層結構的最外層,替每一個響應式物件各存一張 depsMap。

它在 Vue 原始碼裡是一個模組層級的全域變數,整個應用共用一份,是所有依賴關係真正的落腳處。三層讀起來就是一句話:哪個物件(targetMap)的哪個屬性(depsMap)有哪些 effect 在等(dep)。這一層也是唯一需要弱引用的,因為只有它的 key 是使用者的物件、生命週期不由框架決定。

相關術語: depsMap (裡面存的值)、WeakMap (採用的型別)

出處:第 9 段「多個物件與 targetMap」

WeakMap

弱引用映射表

key 只能是物件(或未註冊的 symbol)、且對 key 只持弱引用的 Map。

弱引用的意思是它不會阻止垃圾回收:key 物件一旦沒有其他地方引用,這一筆連同 value 會被自動清掉。代價是 WeakMap 不可迭代、沒有 size——因為內容隨時可能被回收,列舉出來的結果無法保證穩定。用來替「別人的物件」掛額外資料而不想干涉它的生死時,它幾乎是唯一正確的選擇,Vue 的 targetMap 正是這個場景。

相關術語: Map (的弱引用版本)、garbage collection (允許發生)、memory leak (用來避免)

出處:第 9 段「多個物件與 targetMap」

target

目標物件

被追蹤依賴的那個原始響應式物件,例如 product 或 user。

這個名字不是隨便取的,它來自 Proxy 攔截函式的第一個參數:get(target, key) 裡的 target 就是被代理的原始物件。因此在真實的 Vue 裡,track(target, key) 的兩個參數都是 Proxy 直接遞過來的,函式簽名幾乎是照抄攔截器。知道這個由來,targetMap 這個名字就從「奇怪的術語」變成「照著呼叫端命名」。

相關術語: Proxy (名字的來源)、targetMap (當作 key)

出處:第 9 段「多個物件與 targetMap」

garbage collection

垃圾回收

執行環境自動釋放「已經沒有任何引用指向」的物件所佔記憶體的機制。

判斷標準是可達性:從全域變數等根節點出發走不到的物件就可以回收。關鍵在於「被一個全域結構引用著」等於永遠走得到、永遠不會被回收,這正是全域 targetMap 的風險。弱引用是唯一能讓一筆記錄「存在,但不算數」的手段。

相關術語: memory leak (失效時的結果)、WeakMap (配合的工具)

出處:第 9 段「多個物件與 targetMap」

memory leak

記憶體洩漏

已經用不到的物件因為仍被某處引用而無法回收,導致記憶體持續增長。

在前端最典型的來源就是全域集合:事件監聽器沒移除、快取沒清、或是像這裡一樣把物件登記進一張永不清空的表。單次看不出來,但在長時間執行的 SPA 裡每次路由切換都累積一份,最後就是頁面愈開愈慢。用 WeakMap 存放這類「附掛資料」,等於把清理責任交還給垃圾回收器,不必手工維護。

相關術語: garbage collection (被它阻止)、targetMap (潛在的來源)

出處:第 9 段「多個物件與 targetMap」

見仁見智 6:54
「A weak map is simply a map where the keys are objects.」
這個定義抓錯重點。WeakMap 和 Map 真正的差別是「對 key 只持弱引用、可被垃圾回收」,而不只是 key 的型別限制;正是這一點才讓 Vue 敢用一個全域 targetMap 而不洩漏記憶體。另外自 ES2023 起,未註冊的 symbol 也可以當 WeakMap 的 key,所以「key 必須是物件」這句在今天也不再精確。
依據: TC39 Symbols as WeakMap keys(ES2023);MDN WeakMap(weakly held keys)
留給下一段 三層結構的圖完整了,WeakMap 的用法也示範過了。但 track 和 trigger 目前只收 key、不認得 target,多出來的那一層還沒進到程式碼裡——下一段要補上最後一層實作。

10. 把 targetMap 寫成程式碼 7:34–9:04

track 變成三層:先用 target 拿 depsMap,沒有就建;再用 key 拿 dep,沒有就建;最後把 effect 加進 dep。trigger 反過來,先確認這個物件有沒有 depsMap,沒有就直接 return,有才看這個屬性有沒有 dep。實測 track(product,'quantity')、改 quantity、trigger(product,'quantity'),total 變 15。

完整版 track / trigger 的程式碼,兩層 get-or-create 巢狀在一起,是這門課要你帶走的成品。
8:20 · 完整版 track / trigger 的程式碼,兩層 get-or-create 巢狀在一起,是這門課要你帶走的成品。
承上 承上:上一段畫完三層圖也示範了 WeakMap,但 track / trigger 還停在只認 key 的版本;這一段把 target 這個參數補進去,完成整個儲存端。

推理因為上一段多出來的那一層還沒落地,所以作者接著把兩個函式都改成收 (target, key):track 先用 target 去 targetMap 拿 depsMap,沒有就建一張新的 Map 存回去;再用 key 拿 dep,沒有就建一個新的 Set 存回去;最後 dep.add(effect)。trigger 反過來走:先看這個物件有沒有 depsMap,沒有就直接 return;有才看這個屬性有沒有 dep,有就走訪每個 effect 執行。實測 track(product, 'quantity')、跑 effect 得到 10、把 quantity 改成 3、再 trigger(product, 'quantity'),total 變成 15。

AI 補充這段程式碼的骨架其實只是同一個模式做兩次:track 裡兩層都是「get → 沒有就建 → set 回去」(見第 8 段),差別只在外層建的是 Map、內層建的是 Set。trigger 則是兩層防禦性檢查,而且第一層用的是提早返回而不是巢狀 if——它對應的語意很明確:這個物件從頭到尾沒被 track 過,那就沒有任何人在等它,直接走人。至於為什麼 track 建立、trigger 只檢查:因為登記是「發生了才需要位置」,通知是「沒位置就代表沒人要通知」,兩邊的預設立場天生相反。這裡也是整支影片的成果與缺口交界處。三層結構完全可用了,但 track(product, 'quantity') 和 trigger(product, 'quantity') 都是人手打進去的:effect 裡讀了 product.price 卻沒替 price 登記,所以現在改 price 不會有任何反應;而每改一個值都要記得自己呼叫 trigger,比直接重算一遍還麻煩。這正好說明了為什麼「自動化」不是錦上添花,而是這套設計成立的必要條件。

early return

提早返回

在函式開頭先處理不成立的情況並直接 return,讓主要邏輯不必再往內縮排。

又叫 guard clause(防衛語句)。好處除了少一層縮排,更在於把「什麼情況根本不用做」明確寫在最前面,讀者不必讀完整個函式才知道有這個出口。trigger 裡的 if (!depsMap) { return } 把「這個物件沒有任何依賴」這個完全正常的狀態,和真正要做事的路徑乾淨地分開。

相關術語: trigger (用於保護)

出處:第 10 段「把 targetMap 寫成程式碼」

留給下一段 引擎的儲存端到此完工,也親眼看到 total 從 10 變成 15。但每次都要手動 track、手動 trigger。最後一段要把三層結構收成三句話,並點名還缺的那一半是什麼。

11. 三層結構回顧與下一課 9:04–9:35

作者回到圖上收束:targetMap 存每個響應式物件的屬性依賴,depsMap 存每個屬性的依賴,dep 是一組值改變時要重跑的 effect。到此只解決了「怎麼存」,還沒解決「怎麼自動重跑」——那是下一課的第二塊大拼圖。

最後這張三層 diagram 是整支影片的總結圖,一張圖對應三個名詞。
9:12 · 最後這張三層 diagram 是整支影片的總結圖,一張圖對應三個名詞。
承上 承上:上一段完成了完整的三層程式碼,但也暴露出「什麼都要手動呼叫」的缺口;這一段作者回到同一張圖收束,並正面點出那個缺口。

推理因為上一段把所有零件都拼齊了,所以作者接著把最後這張三層圖再放一次,一層給一句定義:target map 存的是每個響應式物件的屬性依賴,deps map 存的是每個屬性的依賴,dep 是一組在值改變時應該重跑的 effect。收完之後他誠實劃線——我們已經有辦法儲存不同的 effect,但還沒有辦法自動重跑它們,那是下一課的第二塊大拼圖。

AI 補充把缺的那一半說得更具體,這支影片的位置就清楚了。目前 track 和 trigger 都是人手呼叫,要自動化只需要換掉呼叫的時機:讀取屬性時自動 track、寫入屬性時自動 trigger——而能攔截讀寫的工具就是 Proxy(見第 1 段),這也是為什麼最外層叫 targetMap。但光有攔截還不夠:track 被自動呼叫的那一刻,它得知道「現在正在跑的是哪個 effect」才知道要登記誰,所以真實的 Vue 還有一個模組層級的 activeEffect 變數,在執行 effect 前後負責記錄與還原。把 Proxy 和 activeEffect 補上,本課這十幾行就會長成 @vue/reactivity 的雛形。也順帶提醒:真實版本另外還要處理巢狀 effect、每次重跑前清掉舊依賴、以及把觸發丟進排程佇列去重,這些都不在這個教學版裡。

activeEffect

當前執行中的副作用函式

一個模組層級變數,記錄「此刻正在執行的是哪一個 effect」,讓 track 知道該登記誰。

它是把手動 track 變成自動 track 的關鍵。執行一個 effect 之前先把 activeEffect 指向它、執行完再還原;於是 effect 執行過程中讀到的任何屬性,其 get 攔截器都能從 activeEffect 讀到「該登記的是誰」,不必由呼叫端傳進來。因為 effect 可能互相巢狀(元件裡有元件),真實的 Vue 用一個堆疊而不是單一變數來管理它。這也是本課十幾行程式碼和原始碼之間最大的一段距離。

相關術語: track (提供登記對象)、effect (指向的東西)

出處:第 11 段「三層結構回顧與下一課」

留給下一段 總結收束:從「JavaScript 不會自己重算」出發,一路長出 effect(要重跑的程式碼)、dep(誰在等這個值)、depsMap(每個屬性各自一份)、targetMap(每個物件各自一份),儲存端的完整心智模型到此閉合,剩下的自動化留給下一課。

4. 總結

作者先用一個最小的 Vue app 把問題問清楚:改一次 price,模板裡三個位置跟著更新,Vue 憑什麼知道?接著他把 Vue 拿掉、換成十行純 JavaScript,證明改了來源後 total 還是舊值——響應式不是語言內建的,是有人做出來的。確認要自己造之後,他把大問題切成兩塊拼圖,這支影片只做第一塊「怎麼把一段計算存起來以便重跑」。從這裡開始,每一步都是同一個節奏:先讓現有方案撞牆,再加剛好一層結構。把 total 的計算包成匿名函式存進 effect,需要一個放它的地方,於是有了 dep(用 Set,因為它天生去重);一個 dep 服務不了物件的多個屬性,於是上面加一張以屬性名為 key 的 depsMap;一張 depsMap 只屬於一個物件,於是再上面加一張以物件本身為 key 的 targetMap(WeakMap)。每加一層都是先畫圖再翻成程式碼,而且名字全部沿用 Vue 3 原始碼——effect、track、trigger、dep、depsMap、targetMap,所以你學到的不是玩具,是能直接對照原始碼的心智模型。最後他回到同一張三層圖收束,也誠實指出成品的缺口:track 和 trigger 都還得手動呼叫。

勘誤總整理

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

段落原話(transcript 逐字)說明
2. Vue 怎麼知道要更新畫面
1:00
「And it starts by taking a look at a simple Vue app.」這裡搭配的投影片用的是 Vue 2 的建立方式 var vm = new Vue({ el: '#app', ... }),但這是一門 Vue 3 課程。Vue 3 已移除全域的 new Vue() 建構式,改成 createApp({ ... }).mount('#app');照著螢幕寫在 Vue 3 專案裡會直接壞掉。這只影響範例語法,不影響後面要講的響應式機制。
依據: Vue 3 遷移指南 Global API: createApp(new Vue 已移除)
6. 用 Set 當儲存空間 dep
3:13
「we'll use a dep variable which stands for dependency, and it's a new set.」在影片當時的 Vue 3.0–3.3 裡,dep 確實是一個 Set<ReactiveEffect>;但這一點今天已經過時。Vue 3.4 把 dep 改成 Map(effect → trackId 版本號)以支援依賴去重與版本比對,Vue 3.5 再把它改寫成 Dep 類別 + 雙向鏈結串列(Link 節點),Set 已經不存在了。targetMap 與 depsMap 這兩層仍在,所以本課的三層心智模型依然適用,只有最內層的容器換過。
依據: Vue 3.4(2023-12)、3.5(2024-09)reactivity 重寫;packages/reactivity/src/dep.ts
4. 從零打造響應式引擎
1:57
「in the same exact way, that Vue 3 does reactivity.」「一模一樣」要打點折。三層結構(targetMap → depsMap → dep)與命名確實和當時 Vue 3.0–3.3 的原始碼一致,但課程版本省略了 activeEffect 堆疊、巢狀 effect、舊依賴清除、排程佇列、ref 與 computed 等大量真實邏輯。而且 Vue 3.4(2023-12)與 3.5(2024-09)兩次重寫 reactivity 之後,內部細節已經和影片不同,「一模一樣」在今天更接近「概念上一致」。
依據: Vue 3.4 / 3.5 reactivity 重寫,packages/reactivity/src/dep.ts
9. 多個物件與 targetMap
6:54
「A weak map is simply a map where the keys are objects.」這個定義抓錯重點。WeakMap 和 Map 真正的差別是「對 key 只持弱引用、可被垃圾回收」,而不只是 key 的型別限制;正是這一點才讓 Vue 敢用一個全域 targetMap 而不洩漏記憶體。另外自 ES2023 起,未註冊的 symbol 也可以當 WeakMap 的 key,所以「key 必須是物件」這句在今天也不再精確。
依據: TC39 Symbols as WeakMap keys(ES2023);MDN WeakMap(weakly held keys)

5. 推薦三個下一步

1. 往下挖深:Proxy 與 activeEffect 怎麼讓 track / trigger 自動發生

這支影片結束時 track 和 trigger 都還要手動呼叫,作者自己點名這是下一課的第二塊拼圖。補上 Proxy 的 get / set 攔截與 activeEffect 這個全域指標,整個引擎才會自己動起來。

YouTube 搜尋:Vue 3 reactivity Proxy Reflect activeEffect Vue Mastery Vue 3 Reactivity course lesson 2 build reactivity from scratch effect Proxy

2. 往旁邊對照:Vue 2 的 Object.defineProperty 與 Signals 的做法差在哪

作者強調 Vue 3 的做法跟 Vue 2 完全不同,但沒說 Vue 2 錯在哪、Proxy 又補了什麼。把 defineProperty、Proxy 與 Solid / Angular Signals 三種依賴追蹤放在一起比,才知道這套三層結構是選擇而不是唯一解。

YouTube 搜尋:Vue 2 vs Vue 3 reactivity Object.defineProperty Proxy signals vs virtual dom reactivity explained SolidJS signals fine-grained reactivity

3. 往上應用:把 @vue/reactivity 單獨拿出來用,或用它讀懂效能問題

影片開頭說 reactivity 在 Vue 3 是獨立模組、可以用在 Vue 以外的地方。學會 reactive / ref / effect / computed 這組 API,既能拿去做非 Vue 專案的狀態管理,也能用「哪些 effect 被重跑」的角度診斷不必要的重新渲染。

YouTube 搜尋:@vue/reactivity standalone outside Vue Vue 3 ref reactive computed watchEffect deep dive Vue 3 performance unnecessary re-render reactivity debug

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