Vue 3 的響應式引擎:用 effect / track / trigger 與三層 targetMap 從零把依賴存起來
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(11)
1. Outline
- 起點 · 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 目前都得手動呼叫,自動化是下一課的事。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- 為什麼要懂 Vue 3 響應式 → 動機講完了,但「Vue 3 到底怎麼做」還一個字都沒說。作者留下的線索是:要看懂新做法,得先把問題問對——Vue 究竟是憑什麼知道有哪些地方需要更新?下一步要用一個最小的 app 把這個問題變成看得見的東西。
- Vue 怎麼知道要更新畫面 → 問題問出來了:Vue 知道該更新哪三個地方。但作者還沒說這件事為什麼需要特別做——也許 JavaScript 本來就會幫你重算?下一段要把 Vue 整個拿掉,用純 JavaScript 驗證這個假設。
- 純 JavaScript 沒有響應式 → 確認了語言不會幫忙,唯一的出路是把「算 total 的那段程式碼」留起來以便重跑。但留在哪裡、由誰在什麼時候重跑,都還是空白——下一段作者要正式宣告要自己造這個東西,並把大問題切成第一個做得動的小問題。
- 從零打造響應式引擎 → 子問題定下來了:把 total 的計算存起來。要做到這件事至少需要三樣東西——一段可以被存的程式碼、一個存放處、一個把存起來的東西重跑的動作。下一段作者要給這三樣東西名字,而且是 Vue 原始碼裡的名字。
- effect、track、trigger 三個名字 → 三個名字都到位了,但畫面右邊的 storage 到目前為止還只是一個藍色方框——它到底是陣列、物件還是別的東西?下一段要把這個方框換成真正的資料結構,而選哪一種是有理由的。
- 用 Set 當儲存空間 dep → 現在只有一個全域的 dep,意思是 price 和 quantity 共用同一票 effect:改 price 會連只用到 quantity 的程式碼一起重跑。但真實物件有很多屬性,下一段要讓每個屬性各自擁有一個 dep。
- 每個屬性各自一個 dep → 圖已經畫好:depsMap 的每個 key 是屬性名、value 是一個裝 effect 的 Set。但 track 和 trigger 目前還是上一版、只認得那個唯一的 dep。下一段要把這張圖翻成程式碼。
- 把 depsMap 寫成程式碼 → 同一個物件的不同屬性現在可以各自追蹤了。但整張 depsMap 是個全域變數、暗地裡只屬於 product:一旦再來一個 user 物件,它的屬性依賴就無處可放。下一段要在 depsMap 上面再加一層。
- 多個物件與 targetMap → 三層結構的圖完整了,WeakMap 的用法也示範過了。但 track 和 trigger 目前只收 key、不認得 target,多出來的那一層還沒進到程式碼裡——下一段要補上最後一層實作。
- 把 targetMap 寫成程式碼 → 引擎的儲存端到此完工,也親眼看到 total 從 10 變成 15。但每次都要手動 track、手動 trigger。最後一段要把三層結構收成三句話,並點名還缺的那一半是什麼。
- 三層結構回顧與下一課
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 完全不同,是全新內容。
推理因為觀眾多半停在「會用」的層次,所以作者一開場不講技術,先講四個回報:看懂底層設計、除錯時讀得懂 stack trace、把 reactivity 當獨立模組單獨使用、以及看得懂原始碼好貢獻。接著他補上一句最關鍵的定位——Vue 3 的做法跟 Vue 2 完全不同,就算你看過他以前的 reactivity 課程,這裡也是全新內容。
- RVue 2 用 Object.defineProperty 逐屬性改寫,Vue 3 改用 Proxy 攔整個物件→ 存+回想
- CProxy 包住整個物件攔截所有讀寫,是 Vue 3 讓 track / trigger 自動發生的攔截器→ 畫地圖
- RVue 3 的響應式是獨立套件 @vue/reactivity,不裝 Vue 也能單獨用→ 存+回想
AI 補充「完全不同」值得說得更具體一點,因為它決定了後面每一步為什麼要這樣走。Vue 2 是在元件初始化時走一遍 data,把每個屬性用 Object.defineProperty 改寫成 getter/setter,所以它一開始就得知道有哪些屬性——事後新增的屬性偵測不到,陣列索引賦值也偵測不到。Vue 3 改成用 Proxy 攔截整個物件的讀寫,屬性是動態的也攔得到。而第三個理由(reactivity 是獨立模組)在架構上是真的:Vue 3 把它拆成一個叫 @vue/reactivity 的套件,不裝 Vue 也能單獨 npm 安裝來用。這支影片要重建的,就是這個套件內部真正的資料結構。
2. Vue 怎麼知道要更新畫面 1:00–1:30
從一個最簡單的 Vue app 出發:模板裡用到 price、也用到依賴 price 的 computed 屬性。當 price 改變,Vue 就自動更新模板裡三個用到它的地方。作者把整門課的問題定成一句話:Vue 到底怎麼知道該更新哪些東西?

推理因為上一段只給了學習動機、沒給問題,所以作者接著挑一個小到不能再小的例子:data 裡只有 price 和 quantity,模板裡用了 price、用了 price * quantity,還用了一個依賴兩者的 computed 屬性。他改一次 price,指著模板說「這裡、這裡,還有這個 computed 又更新了模板這裡」,把整門課的問題定成一句話——Vue 到底怎麼知道該更新哪些東西?
- Cdependency:某段程式碼執行時讀到某值,值變時該程式碼必須重跑;依賴會串成鏈→ 畫地圖
- E最小 app:改一次 price,模板兩處直接依賴 + 一處經 computed 間接依賴一起更新→ 存+演練
- R投影片的 new Vue({ el, data, computed }) 是 Vue 2 語法,機制講的是 Vue 3→ 存+回想
AI 補充畫面上這個例子有兩件事值得補。第一,作者指的三處其實不是同一種:模板裡兩處是直接依賴 price,第三處是先依賴 computed 屬性 totalPriceWithTax、再由它依賴 price——依賴是會串成鏈的,中間節點自己也要被追蹤。這條鏈之所以重要,是因為它排除了「掃描整個模板重畫一次」這種土法:Vue 更新的粒度是「用到這個值的那幾個地方」,不是整頁。第二,投影片上的程式碼寫的是 var vm = new Vue({ el: '#app', data, computed }),那是 Vue 2 的寫法;機制講的是 Vue 3,但範例語法沒跟著換。看的時候把注意力放在依賴關係上,不要照抄這段初始化程式碼。
術語:computed property
「And it starts by taking a look at a simple Vue app.」
3. 純 JavaScript 沒有響應式 1:30–1:50
作者把同樣的邏輯改寫成純 JavaScript:price 乘 quantity 得到 total,印出來是 10;接著改掉 price,再印 total 仍然是 10。JavaScript 不會自己重算,這證明響應式不是語言本來就有的能力,得自己做出來。

推理因為上一段的問題必須先確認它真的是個問題,所以作者接著寫下最短的反例:price = 5、quantity = 2、total = price * quantity,印出來是 10;接著把 price 改成 20,再印一次 total,還是 10。他下的結論很短——它不會被更新,這裡沒有任何響應式。
- Ceager evaluation:let total = price * quantity 當下算完只存結果,「關係」在賦值瞬間就用完→ 畫地圖
- E純 JS:price=5、quantity=2,total=10;改 price=20 再印 total 還是 10→ 存+演練
AI 補充這個反例的關鍵不在「JavaScript 很笨」,而在賦值的語意。let total = price * quantity 這一行做的是:現在就把 price * quantity 算出來,把算出來的那個數字 10 放進 total。total 存的是一個結果,不是「price 乘 quantity」這條關係;乘法在賦值那一瞬間就用完了,之後 price 變成什麼都與 total 無關。看清楚這點,解法的形狀就跟著確定了:既然關係本身沒被保存,那就得想辦法把「算 total 的那段程式碼」原封不動地留下來,等來源變了再執行一次。這也解釋了為什麼後面第一件事是把計算包成函式——函式是 JavaScript 裡把「一段還沒發生的計算」存起來的唯一方式。
術語:eager evaluation
4. 從零打造響應式引擎 1:50–2:12
作者宣告接下來兩課要從零打造一個響應式引擎,而且做法會和 Vue 3 內部完全一樣。第一個要回答的子問題是:怎麼把 total 的計算「存起來」,讓 price 或 quantity 改變時可以再跑一次?
推理因為上一段證明了 JavaScript 不會自己重算,所以作者接著宣告:這門課的前兩堂要從零打造一個響應式引擎,而且會用和 Vue 3 一模一樣的方式做。他馬上把大問題收窄成一句可執行的提問——我們要怎麼把 total 的計算存起來,好讓 price 或 quantity 更新的時候可以再跑一次?
AI 補充這裡的切法值得停一下。「自動更新」其實藏了兩個獨立的能力:一是把該重跑的程式碼存下來(儲存端),二是在值改變的當下自動去跑它(觸發端)。作者只把第一個立成問題,第二個留給下一課;這也是為什麼接下來所有的 track 和 trigger 都得手動呼叫——不是他忘了,而是自動化被刻意排在後面。先確定「存什麼、存在哪」,自動化才有東西可以觸發;順序反過來會無從下手。另外「從零打造」在這裡不是噱頭:因為結構和名字都跟原始碼對齊,做完之後你看 Vue 原始碼會發現變數名一個一個對得上。
「in the same exact way, that Vue 3 does reactivity.」
5. effect、track、trigger 三個名字 2:12–3:06
作者把 total 的計算包成匿名函式存進變數 effect,用 track 把它存起來,先跑一次算出 total,之後再呼叫 trigger 把存起來的程式碼全部重跑。他特別點名 effect、track、trigger 就是 Vue 3 原始碼裡用的名字。

推理因為上一段把問題縮成「怎麼把計算存起來」,所以作者接著重寫一次程式:把 total = price * quantity 包進一個匿名函式,存進變數 effect——這就是「我們想存起來的程式碼」。然後他排出呼叫順序:先 track()(把 effect 存進 storage),再 effect()(第一次算出 total),之後某個時間點呼叫 trigger()(把 storage 裡所有程式碼跑一遍)。最後他點名這三個字和 Vue 3 reactivity 原始碼裡看到的完全一樣。
- Ceffect:被包成函式、要在它讀到的值改變時重新執行的那段計算→ 畫地圖
- Ctrack:把目前這個 effect 登記進某個值的 dep,表示值變時要重跑它→ 畫地圖
- Ctrigger:把某個值 dep 裡登記過的 effect 全部重新執行一次→ 畫地圖
- A函式是 JavaScript 裡唯一能把「還沒發生的計算」當資料保存的東西→ 批判類比
- R教學版是先 track() 再 effect(),真實 Vue 反過來:執行 effect 讀到值時順手 track→ 存+回想
AI 補充為什麼一定要「包成函式」,值得說白:total = price * quantity 是一個敘述,寫下去就執行完了,你沒辦法把一個已經執行完的敘述交給別人保管;但 () => { total = price * quantity } 是一個值,可以放進變數、塞進集合、之後再呼叫。函式是 JavaScript 裡唯一能把「還沒發生的計算」當成資料傳來傳去的東西,這是整個引擎能成立的語言前提。另外看畫面上的順序:track() 在 effect() 之前,是「先登記、再執行」。真實的 Vue 反過來——它在執行 effect 的過程中,因為讀到了 price 才順手登記。這裡先用手動呼叫代替那個自動登記的時機,是刻意的簡化,也是後面唯一還沒被解掉的人工成分。
6. 用 Set 當儲存空間 dep 3:06–3:57
作者用一個叫 dep 的 Set 來存 effect,dep 是 dependency 的縮寫;選 Set 是因為它不允許重複值,同一個 effect 加兩次也只會有一份。trigger 就是走訪 dep 裡每個 effect 並執行。實測:改了 quantity 後 total 仍是 10,跑 trigger 才變成 15。


推理因為上一段留下「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。
- Cdep:一個 Set,裝「這個值改變時要重跑的所有 effect」,名字是 dependency 縮寫→ 畫地圖
- E模板三處讀 price,同一個渲染 effect 會被 track 三次;用陣列會重繪三次,Set 只留一份→ 存+演練
- RSet 判重複用 ===:只有同一個函式參考才算重複→ 存+回想
- Aclosure 補上 eager evaluation 缺的另一半:effect 抓的是變數本身,重跑自然讀到新值→ 批判類比
- Econsole 實測:total 10 → quantity 改 3 仍 10 → 呼叫 trigger() → 15→ 存+演練
AI 補充去重為什麼值得為它挑一種資料結構?因為在真實情境裡重複登記是常態而非例外:模板裡如果三個地方都讀到 price,同一個渲染 effect 就會被 track 三次。用陣列存,price 改一次就會重繪三次;如果那個 effect 又寫回了自己讀過的值,還可能滾成無窮迴圈。Set 把去重變成資料結構的天然性質,呼叫端不必先檢查存不存在。但要注意 Set 判定重複用的是同一性(===):只有「同一個函式參考」才算重複,兩個內容一模一樣但分別建立的箭頭函式會被當成兩個 effect。另外,這個 demo 之所以跑得動,是因為 effect 是一個閉包——它抓的是外層的 price 和 quantity 變數本身,而不是當初的值,所以重跑時讀到的自然是新值。前一段提到的「值不會自己更新」在這裡被閉包補上了另一半:關係被保存在函式裡,只差有人按下重播。
「we'll use a dep variable which stands for dependency, and it's a new set.」
7. 每個屬性各自一個 dep 3:57–5:02
真實的物件有很多屬性,price 和 quantity 各自需要一個 dep。作者把兩個值包成 product 物件,並提出 depsMap:一個 Map,key 是屬性名稱(quantity、price),value 是該屬性的 dep,也就是一組要重跑的 effect。


推理因為上一段的 dep 是全域唯一的,所以作者接著把場景升級:先把 price 和 quantity 收進一個 product 物件,指出每個屬性都需要自己的一組 effect,然後問「要怎麼替每個屬性各存一個 dep」。他的答案是再包一層——depsMap,一個 Map,key 用物件的屬性名稱(quantity、price),value 就是各自的那個 dep。他也順手把 dep 的定義說死:一組在值改變時應該重跑的 effect,型別是 Set。
- CdepsMap:一個 Map,key 是屬性名稱、value 是該屬性的 dep,讓同物件的各屬性各自追蹤→ 畫地圖
- RdepsMap 的 key 用屬性「名稱」不用值:值會變,名稱在變前變後都指向同一位置→ 存+回想
- R用 Map 不用普通物件:key 任意型別、有 size、順序穩定、不撞 Object.prototype 名字→ 存+回想
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。
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(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。
- P把 depsMap 圖翻成 track(key) / trigger(key):get → 沒有就建 → set 回去 → add→ 練習
- RdepsMap.set(key, (dep = new Set())) 一行做三件事:建 Set、dep 指向它、存進 map→ 存+回想
- Rtrigger 裡的 if (dep):從沒被 track 的屬性 get 回 undefined,「沒人在等」是正常狀態不是錯誤→ 存+回想
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
9. 多個物件與 targetMap 6:07–7:34
如果不只 product,還有 user 物件呢?需要再上面一層 Map,key 是響應式物件本身。Vue 3 把它叫 targetMap,型別是 WeakMap——一種 key 必須是物件的 Map。作者用 console 示範 WeakMap 的 set 與 get,並先賣個關子:為什麼叫 target,下一課才會說。


推理因為上一段的結構只服務得了一個物件,所以作者接著問「如果有多個響應式物件呢」,並在圖上多畫一個 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」,說下一課就會清楚。
- CtargetMap:最外層 WeakMap,key 是響應式物件本身、value 是它的 depsMap→ 畫地圖
- CWeakMap:key 只能是物件,且對 key 只持弱引用——沒人引用時可連同整張 depsMap 被回收→ 畫地圖
- EtargetMap 若用普通 Map:全域永遠活著,元件卸載一千次就洩漏一千份→ 存+演練
- Rtarget 這個名字來自 Proxy 攔截函式的第一個參數:被代理的原始物件→ 存+回想
AI 補充作者對 WeakMap 的定義只講了「key 是物件」,這剛好漏掉了名字裡最重要的那個字。weak 指的是這張表對 key 只持有弱引用:如果程式其他地方都不再引用 product,垃圾回收就可以把它連同這裡登記的整張 depsMap 一起收掉。這才是 Vue 非用 WeakMap 不可的理由——targetMap 是一個全域、永遠活著的變數,如果用普通 Map,每個曾經變成響應式的物件都會被它永久扣住,元件卸載一千次就洩漏一千份,這是教科書等級的記憶體洩漏。順帶補上作者賣關子的那個答案:target 這個名字來自 Proxy(見第 1 段)攔截函式的第一個參數,也就是「被代理的原始物件」。等自動追蹤上場時,track 拿到的 target 正是從 Proxy 傳進來的那個物件,名字就自然對上了。
「A weak map is simply a map where the keys are objects.」
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。

推理因為上一段多出來的那一層還沒落地,所以作者接著把兩個函式都改成收 (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。
- Ptrack / trigger 改收 (target, key):track 兩層「沒有就建」,trigger 兩層檢查加 early return→ 練習
- Atrack 遇到沒有就建立、trigger 遇到沒有就走人:登記與通知的預設立場天生相反→ 批判類比
- R本課成品最大的人工成分:key 是手打字串,effect 讀了 product.price 卻沒人替 price 登記→ 存+回想
AI 補充這段程式碼的骨架其實只是同一個模式做兩次:track 裡兩層都是「get → 沒有就建 → set 回去」(見第 8 段),差別只在外層建的是 Map、內層建的是 Set。trigger 則是兩層防禦性檢查,而且第一層用的是提早返回而不是巢狀 if——它對應的語意很明確:這個物件從頭到尾沒被 track 過,那就沒有任何人在等它,直接走人。至於為什麼 track 建立、trigger 只檢查:因為登記是「發生了才需要位置」,通知是「沒位置就代表沒人要通知」,兩邊的預設立場天生相反。這裡也是整支影片的成果與缺口交界處。三層結構完全可用了,但 track(product, 'quantity') 和 trigger(product, 'quantity') 都是人手打進去的:effect 裡讀了 product.price 卻沒替 price 登記,所以現在改 price 不會有任何反應;而每改一個值都要記得自己呼叫 trigger,比直接重算一遍還麻煩。這正好說明了為什麼「自動化」不是錦上添花,而是這套設計成立的必要條件。
11. 三層結構回顧與下一課 9:04–9:35
作者回到圖上收束:targetMap 存每個響應式物件的屬性依賴,depsMap 存每個屬性的依賴,dep 是一組值改變時要重跑的 effect。到此只解決了「怎麼存」,還沒解決「怎麼自動重跑」——那是下一課的第二塊大拼圖。

推理因為上一段把所有零件都拼齊了,所以作者接著把最後這張三層圖再放一次,一層給一句定義:target map 存的是每個響應式物件的屬性依賴,deps map 存的是每個屬性的依賴,dep 是一組在值改變時應該重跑的 effect。收完之後他誠實劃線——我們已經有辦法儲存不同的 effect,但還沒有辦法自動重跑它們,那是下一課的第二塊大拼圖。
- R自動化還需要 activeEffect:模組層級變數記錄「此刻正在跑哪個 effect」,track 才知道登記誰→ 存+回想
- R教學版沒處理的三件事:巢狀 effect、重跑前清掉舊依賴、觸發丟進排程佇列去重→ 存+回想
AI 補充把缺的那一半說得更具體,這支影片的位置就清楚了。目前 track 和 trigger 都是人手呼叫,要自動化只需要換掉呼叫的時機:讀取屬性時自動 track、寫入屬性時自動 trigger——而能攔截讀寫的工具就是 Proxy(見第 1 段),這也是為什麼最外層叫 targetMap。但光有攔截還不夠:track 被自動呼叫的那一刻,它得知道「現在正在跑的是哪個 effect」才知道要登記誰,所以真實的 Vue 還有一個模組層級的 activeEffect 變數,在執行 effect 前後負責記錄與還原。把 Proxy 和 activeEffect 補上,本課這十幾行就會長成 @vue/reactivity 的雛形。也順帶提醒:真實版本另外還要處理巢狀 effect、每次重跑前清掉舊依賴、以及把觸發丟進排程佇列去重,這些都不在這個教學版裡。
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
- 相關 —9 JavaScript Concepts That Got Me To Senior Dev · closure 與 garbage collection 講完,這站就用它們把 effect 存起來
- 相關 —Top 15 Frontend Interview Questions for 2026 (wa/ Senior Engineer) · 那邊考 Map、WeakMap 與 GC,這站拿它們當依賴的儲存結構
- 相關 —Embrace Reactive Programming in React and Vue with RxJS - Fahad Ibnay Heylaal · 同樣講值改變怎麼傳播:一個靠依賴追蹤,一個靠 stream
- 相關 —JavaScript Visualized - Closures · Vue 的 effect 就是靠 closure 抓住依賴,這裡先懂它怎麼被保留
- 相關 —JavaScript Visualized - Execution Contexts · 共用 closure:一站是語言機制本身,一站用它做響應式的依賴追蹤
- 接著看 →Vapor Mode is the Future of Vue · Vapor 拿掉 vdom 的本錢就是 effect / track / trigger,那站先把它拆到底