資深前端模擬面試:從效能觀念題到 useRef 實作題的深度驗收

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

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

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

1. Outline

  1. 起點 · Senior Frontend Interview 2026 🎉 | Javascript 🎯 (Mock) [Most Asked Questions]
    一場完整錄下的資深前端模擬面試:前 18 分鐘是 12 題觀念問答(效能、React 內部機制、JavaScript 地基、框架與網路),後 12 分鐘是一道 stopwatch 實作題,用來驗收「答得出來」和「寫得出來」之間的落差。

2. YouTuber 的思維推導

Senior Frontend Interview 2026 🎉 | Javascript 🎯 (Mock) [Most Asked Questions]

Dev. Aditya · 30m36s · 字幕 en · vision=on (使用者指定 vision=true。影片後半是螢幕分享的實作題,畫面上的程式碼是理解卡關點與修正的關鍵,讀圖才看得懂 ref.current、clearInterval 的位置。) · YouTube

預估 vs 實際耗時
階段預估實際
fetch 6s 2s
segment 1m31s 3m30s
shot 56s 4s
analyze 12m30s 15m00s
render 5s –

這支影片是一場真實錄下的資深前端模擬面試,作者(面試官 Dev.Aditya)的推導主軸不是「教觀念」,而是「示範一場面試怎麼由淺入深地測出一個人的深度」。他的出發點是最開放的效能題,因為這題會逼受試者自己攤開知識地圖;接著他從受試者主動說出的名詞(tree shaking)往下追問,確認那是真懂還是背詞。

確認基本盤之後,他把問題往「這件事底層怎麼運作」推:reconciliation、瀏覽器渲染流程;再往「同一個目的的兩種手段有何差別」推:debounce vs throttle、useRef vs useState。接著轉進 JavaScript 語言本身的 closure 與 this,測的是框架之外的地基。中段他刻意留了一題受試者答不出來的 global error boundary,並在對方繞了三次之後直接給答案(componentDidCatch),示範面試官在卡關時該怎麼收——不羞辱、給答案、往下走。

理論題收尾在幾個「不要一次全給」的優化題(虛擬列表、code splitting、WebSocket)之後,他把整場交給實作:一題看起來只要五行的 stopwatch。作者真正的結論藏在這一題——他讓受試者依序踩進四個坑(interval id 存在區域變數所以清不掉、hook 巢狀呼叫、只清 interval 沒清 ref、start 沒有守門條件),每一步只給最小的提示,讓觀眾看見「觀念都答得出來」和「手真的寫得出來」之間的落差有多大。所以整支影片的推論是:資深前端的門檻不在能不能講出 closure 的定義,而在能不能把 useRef 這個你剛剛才正確定義過的東西,在有壓力的十分鐘裡正確地用出來。

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

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

  1. 效能優化:從程式碼內到打包外 → 受試者把效能的討論停在「React 大多會自動處理」,但沒有說 React 自動處理的機制到底是什麼。面試官抓住這個缺口,下一題直接問:React 內部是怎麼決定只更新變動的那一塊?
  2. Reconciliation 與 diffing 演算法 → 受試者說明了 React 決定「哪些 DOM 要改」之後就停住了,但沒說 DOM 改完之後瀏覽器還要做什麼才會變成畫面。面試官順著這個沒接上的下半段,把問題交給瀏覽器本身。
  3. 瀏覽器渲染流程:Critical Rendering Path → 到這裡兩段都在談「瀏覽器與框架自己做的事」。面試官接著把問題轉向開發者主動控制的部分:當使用者高頻觸發事件時,你怎麼決定要不要真的執行?
  4. Debounce 與 Throttle 的差別 → 這一段是本場第一次出現「兩個功能相近的工具怎麼選」的題型,而且答案的關鍵在時間與時機。面試官接著把層級從框架 API 往下拉到 JavaScript 語言本身:既然 debounce 要在延遲後還記得原本的參數與計時器,那支撐這件事的語言機制是什麼?
  5. Closure:函式記得外層變數 → closure 說明了函式怎麼決定「看得到哪些變數」,但還有另一個變數的可見性不是由出生位置決定的——this。面試官接著把問題轉到這個唯一的例外。
  6. this 的指向與三種調整方式 → 這一段結束了 JavaScript 語言地基的檢查——closure 與 this 都過關了。面試官接著回到 React,而且挑了一組跟 debounce / throttle 同型的題目:兩個看起來都在「記住一個值」的 hook,差別在哪裡?
  7. useRef 與 useState 的分工 → useRef 與 useState 都是 hooks,但 hooks 是後來才出現的。面試官接著往回追一層:hooks 之前 React 是怎麼處理元件的誕生與消失?
  8. 生命週期方法到 useEffect → 受試者強調自己只用過函式元件與 hooks。面試官記下了這一點,並在下一題設下陷阱——問一個至今只有 class 元件才有原生解法的問題。
  9. SSR 為什麼快,又為什麼利於 SEO → 受試者順利答完框架層的正向題,而且再次展現他熟悉的是 Next.js 的功能而非 React 的底層機制。面試官現在用上一段記下的那條線索設陷阱:問一個只有 class 元件才有原生解法的題目。
  10. 全域錯誤邊界:卡關與正解 → 這一題受試者卡住了,面試官給完答案就換題。他接下來挑的是幾個受試者一定答得出來的優化題,用意是在進入實作前把節奏調回來——而這些題目共享同一個想法:不要一次把全部東西交出去。
  11. 一萬筆列表與 code splitting → 這兩題都在減少「一次送出去的量」,但都還是客戶端主動去要資料。最後一題觀念題把方向反過來:如果需要伺服器主動推資料給你呢?
  12. WebSocket 與請求/回應模型的差別 → 到這裡所有觀念題都問完了,受試者除了 error boundary 之外幾乎全數答對,包含第 7 段那個 useRef 的定義。面試官接著要驗收的是最後也最關鍵的一件事:這些答得出來的觀念,手寫得出來嗎?
  13. 實作題開場:碼表的三個要求 → 第一版的 setInterval id 沒有被存在任何跨 render 存活的地方。下一段面試官按下 stop,這個結構性問題立刻現形。
  14. clearInterval 失效:提示改用 useRef → 受試者拿到了「把 interval 存進 ref」這個方向,開始動手改寫。但他把 useRef 寫進了 useEffect 裡面,下一段編輯器立刻報錯。
  15. hook 不能巢狀,與命名的建議 → 受試者的 ref 寫成了 stopWatchRef 而不是 stopWatchRef.current。下一段面試官先補上這個 .current,然後請他連按四次 start——真正的考點終於登場。
  16. 連按 start 的 bug 與錯誤方向 → 三個方向都被擋掉,而編輯器的型別錯誤已經把答案指出來了:ref 從來沒有被設回 null。下一段面試官揭曉這個 bug,並補上最後一塊拼圖。
  17. 真正的修法:清掉 ref 並在啟動前檢查

3. 逐段說明

Senior Frontend Interview 2026 🎉 | Javascript 🎯 (Mock) [Most Asked Questions]

1. 效能優化:從程式碼內到打包外 0:00–2:55

第一題直接問「你怎麼改善應用程式效能」。答題者建立了一條由內而外的檢查順序:先用 profiling 工具找不必要的 re-render,用 memo / useMemo / useCallback 擋掉;再看 state 結構是否造成連鎖 re-render,把 prop drilling 的狀態上移到 Redux 等全域狀態;再把圖表這類重元件改成 dynamic import 搭配 Suspense。最後才走出程式碼,看 bundle 大小、tree shaking 與 gzip。面試官追問 tree shaking,答題者說明它是把沒被用到的 import 從 bundle 移除。

承上 前情提要裡提到的背景:這是一場資深前端的模擬面試,面試官會用開放題開場。第一題「你怎麼改善應用程式效能」正是這種題型——它不考單一知識點,而是讓受試者自己決定從哪裡切入、切到多深。

推理因為開場題必須讓受試者自己攤開知識地圖,所以面試官挑了範圍最大的效能題;而受試者的回答方式本身就是答案的一部分——他沒有隨口丟名詞,而是建立了一條由內而外的檢查順序:先看元件層(不必要的 re-render,用 memoization 擋掉)、再看狀態層(結構不良與 prop drilling 造成連鎖 re-render,把狀態上移到全域)、再看載入層(重元件改 dynamic import 搭 Suspense),最後才走出程式碼看 bundle 本身(大小、tree shaking、gzip)。面試官在他說出 tree shaking 之後立刻回頭追問定義,這是在確認名詞是背來的還是懂的。

AI 補充這條順序值得記下來,因為它同時是一份除錯清單:從「畫面重畫太多次」往「送給瀏覽器的東西太多」走。受試者省略了一步——他說「用 profiling 工具找不必要的 re-render」,但沒說怎麼判定「不必要」。實務上的判準是:這次 re-render 有沒有讓畫面的任何一個像素改變;React DevTools Profiler 的 highlight updates 可以直接看出哪些元件重畫了卻沒改變外觀。另外他把 memoization 講得像萬用解,但 React.memo / useMemo 本身也有比較成本,只有在元件重、或它的 props 幾乎不變時才划算;先修 state 結構通常比到處包 memo 有效。至於他最後說「用 React 這類框架大多會自動處理」,指的是 bundler(webpack / Vite / Turbopack)在 production build 預設就會做 tree shaking 與壓縮,不是 React 本身做的。

re-render

重新渲染

React 重新執行元件函式、算出新的畫面描述的過程。

re-render 不等於瀏覽器重畫。React 重新執行元件、比對結果之後,只有真的變了的部分才會動到真實 DOM。所以「多餘的 re-render」通常不是畫面卡的直接原因,而是在元件很重、或數量很多時才會累積成問題。常見的觸發來源是 state 更新、父元件 re-render、以及 context 值改變。

相關術語: memoization (用來擋掉)、prop drilling (常見成因)

出處:第 1 段「效能優化:從程式碼內到打包外」

memoization

記憶化

把上一次的結果存起來,輸入沒變就直接沿用,不重算。

在 React 裡有三個入口:React.memo 記住整個元件的輸出、useMemo 記住一個計算值、useCallback 記住一個函式參照。它們共同的前提是「輸入沒變」,而輸入是用淺比較判定的——所以每次 render 都新建的物件或函式當 props 傳下去,memo 就完全失效。這是新手包了 memo 卻沒效果的頭號原因。

相關術語: re-render (用來減少)

出處:第 1 段「效能優化:從程式碼內到打包外」

prop drilling

屬性層層下傳

為了把資料送到深層元件,被迫讓中間每一層都轉手同一個 prop。

問題不只在寫起來煩,而在中間層被迫依賴它其實不用的資料——上層值一變,整條路徑上的元件都跟著 re-render。解法有三種層級:把元件組合起來(children)讓資料不必穿層、用 Context 廣播、或搬進 Redux / Zustand 這類全域狀態。受試者直接跳到第三種,但前兩種通常成本更低。

相關術語: re-render (會擴大)

出處:第 1 段「效能優化:從程式碼內到打包外」

dynamic import

動態載入

把一段程式碼延後到真正需要時才向伺服器要,不放進首次載入的 bundle。

語法上是 import() 這個回傳 Promise 的函式;在 React 裡通常包成 React.lazy 再用 Suspense 提供載入中的畫面。它是 code splitting 在原始碼裡的實際寫法——bundler 看到 import() 就會在那裡切出一個獨立的 chunk。圖表、編輯器、地圖這類又大又不是一進站就要用的元件最適合。

相關術語: tree shaking (都在縮 bundle)

出處:第 1 段「效能優化:從程式碼內到打包外」

tree shaking

搖樹優化

打包時把沒有被實際用到的 import 從 bundle 裡移除。

名字來自「搖樹讓枯葉掉下來」。它依賴 ES module 的 import / export 是靜態可分析的——bundler 在不執行程式的情況下就能推斷哪些 export 沒人用。所以用 CommonJS 的 require、或函式庫有 side effect 而 package.json 沒標 sideEffects: false 時,tree shaking 會失效。這也是為什麼 lodash 建議寫 import debounce from 'lodash/debounce' 而不是整包引入。

相關術語: bundle (作用對象)

出處:第 1 段「效能優化:從程式碼內到打包外」

bundle

打包檔

把散落的原始檔與相依套件合併壓縮後,實際送到瀏覽器的 JavaScript 檔案。

bundle 大小直接決定使用者要下載多久、瀏覽器要解析執行多久,是前端效能最好量化的單一指標。實務上會拆成多個 chunk:共用的 vendor、每一頁各自的 chunk、以及 dynamic import 切出來的部分。用 webpack-bundle-analyzer 或 Vite 的 rollup-plugin-visualizer 可以看出哪個套件佔掉最多體積。

相關術語: tree shaking (被它縮小)

出處:第 1 段「效能優化:從程式碼內到打包外」

留給下一段 受試者把效能的討論停在「React 大多會自動處理」,但沒有說 React 自動處理的機制到底是什麼。面試官抓住這個缺口,下一題直接問:React 內部是怎麼決定只更新變動的那一塊?

2. Reconciliation 與 diffing 演算法 2:55–3:48

面試官從「效能」轉向「React 為什麼能只更新一部分」。答題者說明 React 維護一份 virtual DOM(真實 DOM 的副本),變動時拿新舊 virtual DOM 比對,用 diffing 演算法找出改變的部分再更新真實 DOM,整個比對加更新的流程就叫 reconciliation。

承上 承上:上一段停在「React 會自動幫你處理大部分更新」這個沒說完的斷言。這一段就是把那句話拆開——React 之所以能只更新變動的部分,靠的是 reconciliation。

推理因為上一段整段都在談「少做一點事」,所以面試官接著問 React 內部本來就在做的那件「少做一點事」是什麼。受試者的推導是三步:React 在記憶體裡維護一份 virtual DOM,也就是真實 DOM 的輕量副本;狀態變動時產生一份新的 virtual DOM,拿去和舊的比對;比對用的是 diffing 演算法,找出差異後只把那部分寫回真實 DOM。他明確把「比對 + 更新」這整條流程命名為 reconciliation,把演算法本身命名為 diffing。

AI 補充這個回答正確但少了關鍵的一步:為什麼要多維護一份副本?因為直接操作真實 DOM 很貴——每次寫入都可能觸發瀏覽器重算 layout。virtual DOM 是用便宜的 JavaScript 物件比對,換掉昂貴的 DOM 寫入。另外受試者沒提到 diffing 演算法的兩個關鍵假設,而這正是上一段 re-render 問題的根源:第一,不同型別的元素直接整棵重建,不往下比;第二,同一層的子元素靠 key 來配對,這就是列表沒給 key、或拿陣列 index 當 key 會出錯的原因。理解這兩點,才知道為什麼上一段說的「memo 擋 re-render」是在 reconciliation 之前就先剪掉一整棵子樹,效果比讓 diffing 慢慢比對好得多。

術語:diffing algorithm

virtual DOM

虛擬 DOM

React 在記憶體裡維護的一份輕量 JavaScript 物件樹,對應畫面該長什麼樣。

它不是 React 獨有,也不是「比較快」的魔法——直接操作 DOM 永遠比多繞一層快。它的價值在於讓開發者可以每次都描述「畫面現在該長怎樣」,而不必自己算「跟上次比要改哪裡」。React 18 之後內部實際上是 Fiber 樹,可以中斷與恢復比對工作,讓高優先度的更新(例如打字)插隊。

相關術語: reconciliation (比對的對象)

出處:第 2 段「Reconciliation 與 diffing 演算法」

reconciliation

調和

React 比對新舊 virtual DOM、算出最小更新量並寫回真實 DOM 的整個流程。

分成兩個階段:render phase 負責跑元件、建新樹、比對差異,這個階段可以被中斷;commit phase 負責把差異寫進真實 DOM,不可中斷。React 18 的 concurrent 特性都建立在「render phase 可中斷」這件事上。開發模式下元件會被執行兩次,就是為了逼出 render phase 裡不該有的 side effect。

相關術語: diffing algorithm (使用的演算法)、re-render (由它觸發)

出處:第 2 段「Reconciliation 與 diffing 演算法」

diffing algorithm

差異比對演算法

在兩棵樹之間找出差異的演算法,React 用它決定哪些 DOM 節點要動。

通用的樹比對是 O(n³),React 靠兩個啟發式假設壓到 O(n):元素型別不同就整棵重建、同層子元素靠 key 配對。這兩個假設就是實務規則的來源——不要在條件分支裡切換元件型別(會整棵重掛、state 全丟),列表一定要給穩定的 key(用 index 當 key 在排序或刪除時會錯位)。

相關術語: virtual DOM (比對的資料)

出處:第 2 段「Reconciliation 與 diffing 演算法」

留給下一段 受試者說明了 React 決定「哪些 DOM 要改」之後就停住了,但沒說 DOM 改完之後瀏覽器還要做什麼才會變成畫面。面試官順著這個沒接上的下半段,把問題交給瀏覽器本身。

3. 瀏覽器渲染流程:Critical Rendering Path 3:48–4:36

問題再往下一層:瀏覽器拿到 bundle 之後怎麼變成畫面。答題者依序拆解——解析 HTML、解析 CSS、建 DOM、建 CSSOM,接著算 layout、paint,最後交給 GPU 合成並輸出像素,並指出這整條流程叫 critical rendering path。

承上 承上:上一段結束在「React 把差異寫回真實 DOM」,但畫面還沒出現。這一段接著問:DOM 被改之後,瀏覽器要走哪些步驟才會變成螢幕上的像素?

推理因為上一段把責任交棒給了瀏覽器,所以面試官順勢把層級再往下推一層。受試者依序拆出這條流水線:解析 HTML、解析 CSS,分別建出 DOM 與 CSSOM;把兩者合起來算 layout,決定每個元素的位置與大小;接著 paint 畫出各層的內容;最後交給 GPU 合成,輸出成螢幕上的像素。他自己把整條流程命名為 critical rendering path。

AI 補充順序正確,但中間漏了一個對前端最有實務意義的環節:paint 之後、送 GPU 之前還有 compositing(合成)這一層,瀏覽器把畫面拆成多個 layer 分別上傳 GPU,再由 GPU 疊起來。這一層之所以重要,是因為它決定了哪些 CSS 屬性便宜、哪些貴:改 width / top 會回到 layout 重算(reflow),改 background-color 從 paint 開始重來,而改 transform / opacity 只動到合成階段,可以完全交給 GPU。這正是「動畫用 transform 不要用 top / left」這條老規則的來源。另外他也沒提到這條路徑會被阻塞——外部 CSS 是 render-blocking,沒有 defer / async 的 script 會擋住 HTML 解析,這才是上一段講的 bundle 大小之所以會拖慢首次畫面的真正機制。

DOM

文件物件模型

瀏覽器把 HTML 解析成的樹狀結構,是 JavaScript 能操作畫面的介面。

DOM 是解析結果,不是原始 HTML 的複本——瀏覽器會修補未閉合的標籤、補上 tbody 這類隱含元素,所以 DevTools 的 Elements 面板看到的常常跟原始碼不同。它同時是效能瓶頸所在:每次讀取 offsetHeight 這類需要即時版面資訊的屬性,都可能逼瀏覽器提前算一次 layout。

相關術語: CSSOM (並列的另一樹)、virtual DOM (被它映射)

出處:第 3 段「瀏覽器渲染流程:Critical Rendering Path」

CSSOM

CSS 物件模型

瀏覽器把所有 CSS 規則解析後建成的樹,記錄每個節點最終套用的樣式。

CSSOM 必須完整才能開始算 layout——因為後面的規則可能覆蓋前面的,少讀一條就可能算錯。這就是 CSS 被稱為 render-blocking resource 的原因:瀏覽器會等所有 stylesheet 下載解析完才畫第一幀。把首屏必要的樣式內嵌成 critical CSS、其餘延後載入,就是針對這一點的優化。

相關術語: DOM (合併成 render tree)

出處:第 3 段「瀏覽器渲染流程:Critical Rendering Path」

layout

版面計算

根據 DOM 與 CSSOM 算出每個元素在頁面上的確切位置與大小。

又叫 reflow。它的代價是「牽一髮動全身」——改一個元素的寬度可能讓整個子樹重算。最貴的情況是 layout thrashing:在迴圈裡交錯地寫入樣式又讀取 offsetWidth,逼瀏覽器每一圈都同步重算一次。解法是把所有讀取集中在前、寫入集中在後。

相關術語: paint (下一階段)

出處:第 3 段「瀏覽器渲染流程:Critical Rendering Path」

paint

繪製

把每個元素的顏色、邊框、文字、陰影等實際畫成像素資料。

paint 之後還有 composite:瀏覽器把畫面分成多個 layer 上傳 GPU 再疊合。這個分層是效能調校的槓桿點——transform 與 opacity 只影響 composite,不必重跑 layout 與 paint,所以動畫用它們最便宜。用 will-change 可以提示瀏覽器提前分層,但濫用會吃掉大量記憶體。

相關術語: layout (前一階段)

出處:第 3 段「瀏覽器渲染流程:Critical Rendering Path」

critical rendering path

關鍵渲染路徑

從收到 HTML 到螢幕出現第一幀畫面所必經的整條處理流程。

優化它有三個方向:減少路徑上的資源數量(合併、內嵌 critical CSS)、縮小每個資源的體積(壓縮、tree shaking)、縮短關鍵路徑長度(把非必要的 script 加上 defer / async)。FCP、LCP 這些指標量的就是這條路徑走完的時間點。

相關術語: layout (包含的階段)、bundle (會拖慢它)

出處:第 3 段「瀏覽器渲染流程:Critical Rendering Path」

留給下一段 到這裡兩段都在談「瀏覽器與框架自己做的事」。面試官接著把問題轉向開發者主動控制的部分:當使用者高頻觸發事件時,你怎麼決定要不要真的執行?

4. Debounce 與 Throttle 的差別 4:36–6:20

面試官問兩者差異。答題者先說共同目的:減少連續觸發的使用者事件;差別在「怎麼減」。debounce 是等使用者停手後延遲時間到才觸發一次;throttle 是在每段延遲區間內最多觸發一次。他用 300ms 搜尋框舉例:連續輸入時 debounce 只在停下後發一次,而 throttle 在同樣情境會固定間隔各發一次。面試官複述確認 throttle 是固定間隔執行。

承上 承上:上一段講清楚了每一次畫面更新都要走完一整條渲染路徑,代價不低。既然如此,開發者能做的第一件事就是別讓高頻事件每次都觸發那條路徑——面試官因此問起 debounce 與 throttle。

推理因為前三段都在談「一次更新有多貴」,所以面試官接著問「怎麼少觸發幾次」。受試者先點出兩者的共同目的:減少連續的使用者事件;再把差別收在一句話上——debounce 是延遲之後只發一次,throttle 是每段延遲區間內最多發一次。接著他用 300 毫秒的搜尋框把差別演出來:連續打字時 debounce 會一路等到使用者停手、再過 300 毫秒才發一次;同樣的輸入在 throttle 下則會固定每 300 毫秒發一次。面試官複述「throttle 是固定間隔執行」來確認理解一致。

AI 補充受試者的定義正確,但漏了選擇的判準,而那才是面試想聽的:問「中間過程的結果有沒有用」。搜尋建議只有最後一次輸入有意義,用 debounce;捲動位置、滑鼠拖曳、視窗縮放這類過程本身就要即時反饋,用 throttle。另一個他沒提的細節是 leading / trailing——debounce 預設在延遲結束時執行(trailing),但按鈕防連點需要的是 leading:立刻執行第一次、後續忽略。這一點在本片最後的實作題裡會再出現一次,因為受試者遇到連按 start 的問題時第一個想到的正是 debounce。

debounce

防抖

事件停止觸發後等待固定時間,若期間沒有新事件才執行一次。

心智模型是「電梯門」:只要還有人進來就重新等,沒人了才關門。實作上就是每次觸發都 clearTimeout 舊的、再 setTimeout 一個新的。要注意的是若事件永遠不停(例如持續打字),callback 就永遠不會執行,所以搜尋建議通常會再加一個 maxWait 上限。lodash 的 debounce 支援 leading / trailing / maxWait 三個選項。

相關術語: throttle (對照組)

出處:第 4 段「Debounce 與 Throttle 的差別」

throttle

節流

在每段固定長度的時間窗內,最多只讓事件執行一次。

心智模型是「水龍頭開到一半」:不管上游多大,流出來的速率固定。適用於過程本身就有意義的事件——scroll、mousemove、resize。捲動監聽的現代替代方案是 IntersectionObserver 與 ResizeObserver,它們由瀏覽器排程,比自己 throttle 更省。

相關術語: debounce (對照組)

出處:第 4 段「Debounce 與 Throttle 的差別」

留給下一段 這一段是本場第一次出現「兩個功能相近的工具怎麼選」的題型,而且答案的關鍵在時間與時機。面試官接著把層級從框架 API 往下拉到 JavaScript 語言本身:既然 debounce 要在延遲後還記得原本的參數與計時器,那支撐這件事的語言機制是什麼?

5. Closure:函式記得外層變數 6:20–7:07

轉進 JavaScript 語言本身。答題者定義 closure 為:內層函式能記住並存取外層函式的變數,即使外層函式已經執行完畢。他用 count 變數與 increment 內層函式舉例,並點出這在多數其他語言不會發生,是 JavaScript 的特性。

承上 承上:上一段的 debounce 必須在延遲結束後還記得計時器 id 與原本的參數,那個「記得」正是這一段要問的語言機制。面試官把題目從框架層轉進 JavaScript 本身。

推理因為前面幾題都靠框架 API 就能答,面試官需要確認地基是不是也穩,所以問了 closure。受試者的定義是:一個函式能記住並存取它外層函式的變數,即使外層函式早已執行完畢。他用最小的例子演示——外層函式裡有一個 count 變數與一個 increment 內層函式,呼叫 increment 時仍然能存取 count。他還補了一句對照:這在多數其他語言不會這樣運作,是 JavaScript 的特性。

AI 補充定義正確,但少了「為什麼」。關鍵在於 JavaScript 的函式在被建立時,會帶著一個指向它出生環境的參照;只要這個函式還活著,那個環境就不會被垃圾回收,即使建立它的函式已經 return。所以 closure 不是函式「複製」了變數,而是「共用」了同一個變數——這解釋了為什麼在迴圈裡用 var 建立多個函式時,它們最後拿到的是同一個值,而 let 因為每圈都建立新的綁定就不會有這個問題。closure 也是這一場後半實作題的隱形主角:受試者把 setInterval 的 id 存進區域變數時,那個變數確實被 closure 記住了,但每次 re-render 都會建立一份新的,於是 stop 按鈕拿到的是新的那份、清不掉舊的計時器。

closure

閉包

函式連同它出生時所在的作用域一起被保留下來,之後仍能存取那些變數。

三個最常見的用途:用外層函式模擬私有變數(模組模式)、讓 callback 記住當時的參數(debounce、事件處理器)、以及製造工廠函式。最常見的陷阱叫 stale closure——callback 抓住的是它被建立那一刻的變數快照式參照,如果外面已經產生新的變數,它仍指著舊的。React 的 useEffect 依賴陣列漏填時的怪異行為,幾乎都是這個原因。

相關術語: debounce (實作依賴它)、lexical scope (運作前提)

出處:第 5 段「Closure:函式記得外層變數」

留給下一段 closure 說明了函式怎麼決定「看得到哪些變數」,但還有另一個變數的可見性不是由出生位置決定的——this。面試官接著把問題轉到這個唯一的例外。

6. this 的指向與三種調整方式 7:07–8:38

延續語言特性,面試官問 this。答題者給的定義是:this 指向「呼叫該函式的那個物件」,物件呼叫自己的方法時 this 就是該物件,否則落到全域 this。接著補上箭頭函式沒有自己的 this、會從語彙作用域往外拿,以及可用 bind / call / apply 指定 this,並提到 explicit binding 與 implicit binding 兩種綁定。

承上 承上:上一段確立了 JavaScript 靠「函式出生的位置」決定看得到哪些變數。這一段面試官問 this,正是因為 this 是這條規則的例外——一般函式的 this 由呼叫方式決定,不是由出生位置決定。

推理因為上一段已經建立了「作用域由出生位置決定」的框架,所以這一題自然是它的對照。受試者給的定義是:this 指向呼叫該函式的那個物件;用 obj.fn() 呼叫時 this 就是 obj,否則落到全域 this。接著他主動加上一層對比:箭頭函式沒有自己的 this,會從語彙作用域往外拿——也就是回到上一段那條「由出生位置決定」的規則。最後他補上第三層:想要更大的彈性可以用 bind、call、apply 明確指定 this,並把這兩類情況命名為 explicit binding 與 implicit binding。

AI 補充這是全場最完整的一個回答,三層結構清楚。可以補的是完整的優先順序,實務上按這個順序判定:new 綁定(this 是新建的物件)> 明確綁定(bind / call / apply)> 隱式綁定(obj.fn())> 預設綁定(非嚴格模式下是 globalThis,嚴格模式下是 undefined)。而箭頭函式不參與這整套規則,它根本沒有自己的 this。另外要注意一個受試者沒點破的陷阱:隱式綁定會在函式被「取出來」時消失——const f = obj.fn; f() 的 this 就不是 obj 了,這正是把方法直接當 callback 傳進 addEventListener 或 setTimeout 時會出錯的原因,也是 class 元件時代要在 constructor 裡寫 this.handleClick = this.handleClick.bind(this) 的理由。

this

this 關鍵字

函式執行時指向的上下文物件,一般函式由呼叫方式決定。

判定順序:new > bind/call/apply > obj.fn() > 預設(嚴格模式是 undefined,非嚴格是 globalThis)。最容易出錯的是「方法被取出來單獨呼叫」——隱式綁定就消失了。在模組與 class 內部程式碼預設就是嚴格模式,所以拿到的是 undefined 而不是 window,錯誤訊息會是 Cannot read properties of undefined。

相關術語: arrow function (不適用此規則)、lexical scope (箭頭函式改用它)

出處:第 6 段「this 的指向與三種調整方式」

arrow function

箭頭函式

沒有自己的 this、arguments 與 prototype 的函式寫法,this 從外層語彙作用域繼承。

因為 this 由出生位置決定,箭頭函式當 callback 時不需要 bind,這是它在 React 裡普及的主因。但也因此有兩個地方不能用:物件字面量裡的方法(this 會拿到外層而不是該物件)、以及需要動態 this 的事件處理器(拿不到 event.currentTarget 對應的元素)。它也不能當建構函式使用。

相關術語: this (改變其行為)

出處:第 6 段「this 的指向與三種調整方式」

lexical scope

語彙作用域

變數的可見範圍由程式碼撰寫的位置決定,而非執行時的呼叫關係。

JavaScript 用的是語彙作用域,對照組是動態作用域(由呼叫者決定)。這條規則在編譯期就固定了,所以 bundler 才有辦法做靜態分析與 tree shaking。closure 之所以能成立,正是因為函式記住的是它語彙上的外層環境。而 this 是這套規則唯一的例外——除非用箭頭函式把它拉回來。

相關術語: closure (運作基礎)

出處:第 6 段「this 的指向與三種調整方式」

bind / call / apply

明確綁定三方法

三個用來手動指定函式執行時 this 的方法。

call 與 apply 都是立刻執行,差別只在參數傳法:call 一個個列,apply 傳陣列。bind 不執行,而是回傳一個永久綁定好 this 的新函式——所以 bind 過的函式再 bind 一次也改不回來。現代寫法多半用箭頭函式取代 bind,但 bind 仍常見於需要預先鎖定參數的場合(partial application)。

相關術語: this (用來指定)

出處:第 6 段「this 的指向與三種調整方式」

留給下一段 這一段結束了 JavaScript 語言地基的檢查——closure 與 this 都過關了。面試官接著回到 React,而且挑了一組跟 debounce / throttle 同型的題目:兩個看起來都在「記住一個值」的 hook,差別在哪裡?

7. useRef 與 useState 的分工 8:38–9:49

回到 React。答題者用「會不會觸發 re-render」切開兩者:useRef 用來記住一個跨 re-render 仍然存在的值,更新它不會重新渲染;useState 是會觸發 re-render、進而更新畫面的狀態。他用 focus 元素當例子說明何時該選 useRef,面試官複述確認「useState 才觸發 re-render」。

承上 承上:上一段收掉了 JavaScript 語言的地基題。面試官回到 React,而且再次使用第 4 段那個「兩個相近工具怎麼選」的題型——這次是 useRef 與 useState。

推理因為第 4 段的 debounce / throttle 已經證明受試者能用一句話切開相近的概念,面試官再用同型題確認他在 React 上也做得到。受試者抓的判準是「會不會觸發 re-render」:useRef 用來記住一個跨 re-render 仍然存在的值,改它不會重新渲染;useState 是會觸發 re-render、進而讓畫面更新的狀態。他用「想 focus 某個元素、但不想為此重新渲染」當例子。面試官複述確認只有 useState 會觸發 re-render。

AI 補充判準抓得準,但少了一個更根本的差別:useRef 的更新是同步且立即可見的,ref.current = 1 之後馬上讀就是 1;而 useState 的更新是排程的,setState 之後在同一次 render 裡讀到的仍是舊值。這條差別決定了什麼東西該放進 ref——凡是「需要立刻讀到最新值、而且不該影響畫面」的東西:計時器 id、上一次的值、DOM 節點、是否為首次 render 的旗標。反過來說也有一條硬規則:ref 的改變不會通知 React,所以任何要顯示在畫面上的東西都不能只放在 ref 裡。這一段的內容會在二十分鐘後的實作題被逐字驗收——那題的正解就是「把 interval id 放進 ref」,而受試者在這裡明明答對了定義,實作時卻卡住。

useState

狀態 hook

在元件裡宣告一個會觸發 re-render 的狀態值。

setState 是非同步排程的:同一個事件處理器裡連續呼叫會被批次合併成一次 re-render。要根據前一個值計算時務必用函式形式 setCount(prev => prev + 1),否則會抓到同一次 render 裡的舊值——這也是計數器類題目最經典的錯法。React 18 把批次處理擴大到所有情境,包含 setTimeout 與 Promise 裡。

相關術語: useRef (對照組)、re-render (會觸發)

出處:第 7 段「useRef 與 useState 的分工」

useRef

參照 hook

提供一個跨 re-render 保持不變的容器物件,改動它不會觸發重新渲染。

回傳的永遠是同一個 { current: ... } 物件,只有 current 欄位會被你改。兩種用途:綁 DOM 節點(傳給 ref 屬性)、或單純當作元件的實例變數。判準是「這個值變了,畫面需不需要跟著變」——不需要就用 ref。要注意 ref 不能在 render 過程中讀寫,只能在事件處理器或 effect 裡動。

相關術語: useState (對照組)、closure (用來避開陷阱)

出處:第 7 段「useRef 與 useState 的分工」

留給下一段 useRef 與 useState 都是 hooks,但 hooks 是後來才出現的。面試官接著往回追一層:hooks 之前 React 是怎麼處理元件的誕生與消失?

8. 生命週期方法到 useEffect 9:49–11:02

面試官問 React 的生命週期。答題者以 hooks 出現為分界:以前有 componentDidMount、componentDidUnmount 等在元件誕生與銷毀時觸發的方法;hooks 之後改由 useEffect 承接。他說明依賴陣列的三種行為——空陣列時只在掛載時跑、回傳的函式在卸載時跑、陣列內的值變動時重新觸發。

承上 承上:上一段的 useRef 與 useState 都是 hooks。面試官順著這個共同點往回問一層歷史——在 hooks 出現之前,React 靠什麼處理元件的誕生與消失?

推理因為上一段確認受試者熟悉 hooks,面試官用生命週期題測他知不知道 hooks 取代了什麼。受試者以 hooks 的出現為分界線推導:以前有 componentDidMount、componentDidUnmount 這類在元件掛載與卸載時觸發的方法,統稱 lifecycle methods;hooks 之後這些職責改由 useEffect 承接。接著他拆解依賴陣列的三種行為——空陣列時只在掛載時跑一次、回傳的函式在卸載時跑、陣列裡的值變動時重新觸發。

AI 補充推導正確,但有兩處需要修正。第一,正確的方法名是 componentWillUnmount,不是 componentDidUnmount——元件卸載後就沒有實例可以呼叫方法了,所以只有 will 沒有 did。第二,他在描述空依賴陣列時說「會在元件 unmounted 時觸發」,前後文顯示他要說的是 mounted,這是口誤但值得記住正確版本:空陣列 = 只在掛載後跑一次。更重要的是一個他完全沒提到的觀念轉向:useEffect 不是 lifecycle 的一對一替代品。React 官方文件明確指出,思考方式應該從「在哪個時間點做什麼」換成「這個 effect 要跟哪些值同步」。依賴陣列不是效能開關,而是宣告「我用到了這些值」;漏填就會造成上一段提過的 stale closure。React 18 的 StrictMode 在開發模式下會刻意把 effect 掛載、卸載、再掛載一次,就是為了逼出沒有正確清理的 effect。

lifecycle methods

生命週期方法

class 元件上由 React 在特定時機自動呼叫的方法,如掛載後、更新後、卸載前。

主要有 componentDidMount(掛載後)、componentDidUpdate(更新後)、componentWillUnmount(卸載前),以及錯誤處理用的 componentDidCatch 與 getDerivedStateFromError。它們至今沒有被移除,class 元件仍然可用;但函式元件沒有等價寫法,這一點在下一題會變成受試者的關卡。

相關術語: useEffect (被它取代)

出處:第 8 段「生命週期方法到 useEffect」

useEffect

副作用 hook

在 render 完成後執行副作用的 hook,靠依賴陣列決定何時重跑。

官方建議的心智模型是「同步」而非「時間點」:這個 effect 讓某個外部系統跟你的 state 保持一致。回傳的清理函式會在下一次 effect 執行前、以及元件卸載時被呼叫——訂閱、計時器、事件監聽都必須在這裡收掉。純粹為了回應使用者操作而做的事不該放進 effect,應該直接寫在事件處理器裡,這正是本片實作題最後被面試官糾正的地方。

相關術語: dependency array (控制其時機)、lifecycle methods (取代對象)

出處:第 8 段「生命週期方法到 useEffect」

dependency array

依賴陣列

useEffect 的第二個參數,列出 effect 內部用到的所有外部值。

三種寫法三種行為:不傳(每次 render 後都跑)、空陣列(只在掛載後跑一次)、有值(值改變時重跑)。它用 Object.is 做淺比較,所以每次 render 新建的物件或函式放進去會導致無限重跑。eslint 的 react-hooks/exhaustive-deps 規則會檢查漏填——漏填的代價是 effect 抓住舊的變數,也就是 stale closure。

相關術語: useEffect (它的參數)、closure (漏填會出錯)

出處:第 8 段「生命週期方法到 useEffect」

確定錯誤/已過時 10:25
「I think in it's React 13, hooks was introduced」
hooks 不是在 React 13 引入的,而是 React 16.8(2019 年 2 月正式釋出)。React 也從未有過 13 這個版本號——16 之後直接跳到 17、18、19。
依據: React 16.8 release notes, 2019-02-06
見仁見智 10:40
「If the dependency array is empty, whatever is there in the use effect will fire when the component is unmounted」
依賴陣列為空時,effect 內容是在元件「掛載後」執行一次,不是卸載時;卸載時執行的是 effect 回傳的清理函式。受試者下一句就正確描述了清理函式,可見是口誤,但字面說法會誤導。
依據: React 官方文件 useEffect:Reference / Parameters
留給下一段 受試者強調自己只用過函式元件與 hooks。面試官記下了這一點,並在下一題設下陷阱——問一個至今只有 class 元件才有原生解法的問題。

9. SSR 為什麼快,又為什麼利於 SEO 11:02–13:02

問題擴大到 Next.js 的 SSR。答題者說明伺服器先算好、建好 DOM 再把 HTML 交給客戶端,因此渲染是即時的;且 SSR 每一個 request 都會執行,適合資料頻繁且不可預期更新的頁面,例如電商的商品列表頁。面試官追問其他好處,答題者提到效能,面試官補上 SEO,答題者接著說因為 HTML 已在伺服器產好,LCP、FCP 這些指標會比較好看。

承上 承上:上一段確認了受試者的 React 知識停在函式元件與 hooks。面試官先不急著設陷阱,改問一題框架層的 SSR,把場景從單一元件拉大到整個頁面怎麼被送出來。

推理因為前面幾段都在談「畫面在瀏覽器裡怎麼產生」,面試官把同一個問題挪到伺服器端:如果 HTML 在伺服器就先算好,會怎麼樣?受試者的推導是:伺服器先計算資料、建好 DOM 元素、把完成的 HTML 交給客戶端,所以渲染是即時的,不必等客戶端再發 API 請求。接著他補上 SSR 的一個特性——每一個 request 都會重新執行,因此適合資料頻繁且不可預測更新的頁面,並以電商的商品列表頁為例。面試官追問還有什麼好處,受試者只答得出效能,面試官直接補上 SEO;受試者接住並說明因為 HTML 已在伺服器產好,LCP、FCP 這些指標會比較好看。

AI 補充回答方向正確,但把兩件事混在一起了。SSR 讓使用者「更早看到內容」是真的(改善 FCP、LCP),但頁面要能互動還得等 JavaScript 下載並完成 hydration——在那之前按鈕是沒反應的。所以 SSR 改善的是「看得到」的時間,不是「用得動」的時間,有時甚至會讓可互動時間變晚。另外受試者說 SSR 適合「頻繁不可預測更新的頁面」是對的,但漏了 Next.js 真正的決策矩陣:內容幾乎不變用 SSG(build 時產生)、會變但可容忍幾秒延遲用 ISR(定時重新產生)、必須每次都最新或跟使用者身分有關才用 SSR。電商商品列表其實最常用 ISR,因為它比 SSR 便宜得多。至於 SEO,現代 Google 爬蟲確實會執行 JavaScript,但排隊渲染會延遲索引,而且其他社群平台的爬蟲多半不執行 JS——這才是 SSR 對 SEO 真正的價值。

術語:LCP / FCP

SSR

伺服器端渲染

在伺服器上先執行元件、產生完整 HTML 再送給瀏覽器的渲染方式。

對照組有三個:CSR(送空殼 HTML,全部靠瀏覽器執行 JS 產生)、SSG(build 時就產生好靜態 HTML)、ISR(SSG 加上定時或按需重新產生)。SSR 的代價是每個請求都要佔用伺服器運算,且無法直接被 CDN 快取。Next.js App Router 的 React Server Components 又更進一步——某些元件的程式碼根本不會送到瀏覽器。

相關術語: hydration (後續步驟)、SEO (主要受益者)

出處:第 9 段「SSR 為什麼快,又為什麼利於 SEO」

hydration

水合

瀏覽器把 JavaScript 掛上伺服器送來的靜態 HTML,讓它變成可互動頁面的過程。

這是 SSR 常被忽略的下半場:HTML 早早出現,但在 hydration 完成前所有事件處理器都還沒掛上,按了沒反應。若伺服器與客戶端算出的結果不一致就會出現 hydration mismatch 警告,常見成因是直接使用 Date.now()、Math.random() 或 window。React 18 的 selective hydration 讓使用者互動的區塊可以優先水合。

相關術語: SSR (承接其產出)

出處:第 9 段「SSR 為什麼快,又為什麼利於 SEO」

SEO

搜尋引擎最佳化

讓網頁更容易被搜尋引擎正確抓取、理解與排名的做法。

跟渲染方式相關的是可爬取性:Googlebot 雖然會執行 JavaScript,但需要排隊二次渲染,索引會延遲;而 Facebook、X、LinkedIn 這些抓 Open Graph 的爬蟲多半完全不執行 JS,純 CSR 的頁面分享出去會沒有標題與縮圖。這是 SSR / SSG 對 SEO 最實際的價值,比 Core Web Vitals 的分數影響更直接。

相關術語: SSR (常用手段)

出處:第 9 段「SSR 為什麼快,又為什麼利於 SEO」

LCP / FCP

最大內容繪製/首次內容繪製

兩個衡量頁面載入速度的指標:首次畫出任何內容,與畫出最大主要內容的時間點。

FCP 量的是「有東西出現了」,LCP 量的是「主要內容出現了」,後者是 Core Web Vitals 的正式指標,門檻是 2.5 秒內為良好。Core Web Vitals 目前的三項是 LCP、CLS(版面位移)與 INP(互動延遲)——INP 已於 2024 年 3 月取代 FID。SSR 主要改善 LCP 與 FCP,對 INP 幫助有限,因為互動延遲取決於 hydration 之後主執行緒的忙碌程度。

相關術語: critical rendering path (量測其結果)

出處:第 9 段「SSR 為什麼快,又為什麼利於 SEO」

見仁見智 12:54
「First input delay and it will make it better」
兩個問題:一是 FID(First Input Delay)已於 2024 年 3 月被 INP(Interaction to Next Paint)取代,不再是 Core Web Vitals 指標;二是 SSR 不必然改善互動延遲——HTML 提早出現但 hydration 未完成時,使用者的點擊反而可能等更久。
依據: Google web.dev, INP replaced FID as a Core Web Vital in March 2024
留給下一段 受試者順利答完框架層的正向題,而且再次展現他熟悉的是 Next.js 的功能而非 React 的底層機制。面試官現在用上一段記下的那條線索設陷阱:問一個只有 class 元件才有原生解法的題目。

10. 全域錯誤邊界:卡關與正解 13:02–15:40

這題答題者答不出來。他先把 global error boundary 理解成「出錯時顯示的 fallback 畫面」,提到 Next.js 的 error page,又轉去講在 API 共用函式裡用 try/catch。面試官兩次把問題拉回「任何元件出錯時如何全域捕捉、避免整個 app 崩潰」,並問他有沒有 class component 經驗;答題者說只用過 function component。最後面試官直接給出答案:class component 的 componentDidCatch 方法,包住整個應用來攔截錯誤,而 function component 沒有對應寫法。

承上 承上:面試官在第 8 段記下了「受試者只用過函式元件」這條線索,第 9 段先讓他答完擅長的 Next.js 題。這一段就是把那條線索兌現——問一個至今只有 class 元件才有原生解法的題目。

推理因為前面確認了受試者的 React 知識建立在 hooks 之上,面試官問 global error boundary 就是要測那條界線在哪。受試者的推導繞了三圈:先把它理解成「出錯時顯示的 fallback 畫面」,提到 Next.js 有 error page 功能;再轉去講在共用的 API 函式裡用 try / catch 捕捉。面試官兩次把問題拉回精確的定義——任何元件裡發生錯誤時如何全域捕捉、避免整個應用崩潰——並直接問他有沒有 class 元件經驗,受試者答只用過函式元件。他最後猜了 AggregateError,仍不對。面試官於是揭曉:class 元件有 componentDidCatch 方法,用它包住整個應用就能即時攔截錯誤;而函式元件沒有對應的寫法。

AI 補充面試官的正解正確但講得太短,有兩點值得補。第一,error boundary 需要兩個方法搭配:getDerivedStateFromError 負責更新 state 以顯示 fallback UI,componentDidCatch 負責記錄錯誤(送到 Sentry 之類的服務)。第二,也是最實用的一點——實務上今天沒有人手寫 class,大家用的是 react-error-boundary 這個套件,它把 class 包好,對外提供函式元件的 API 與 useErrorBoundary hook。至於受試者提到的 Next.js error page 其實不算離題:App Router 的 error.tsx 就是框架幫你套好的 error boundary,只是它綁定路由層級,無法像手動放置的 boundary 那樣精細地包住某一個 widget。還有一個兩邊都沒提的邊界:error boundary 只攔得住 render 階段的錯誤,事件處理器、setTimeout、以及非同步程式碼裡的錯誤它一律接不到——那些仍然要靠 try / catch,也就是受試者最先講的那個方向。所以他其實答對了一半,只是沒能把兩半接起來。

error boundary

錯誤邊界

能攔截其子元件樹在 render 期間拋出的錯誤、並顯示備用畫面的 React 元件。

只有 class 元件能成為 error boundary,實作 getDerivedStateFromError(更新 state 顯示 fallback)與 componentDidCatch(記錄錯誤)。它攔不到四種錯誤:事件處理器、非同步程式碼、伺服器端渲染、以及 boundary 自己拋出的錯誤。實務上直接用 react-error-boundary 套件,並且分層放置——整站一個保底、每個主要區塊各一個,避免單一 widget 壞掉就白屏。

相關術語: componentDidCatch (核心方法)、lifecycle methods (屬於其一)

出處:第 10 段「全域錯誤邊界:卡關與正解」

componentDidCatch

錯誤捕捉生命週期

class 元件的生命週期方法,子樹在 render 期間拋錯時被呼叫。

接收兩個參數:error 與含有 componentStack 的資訊物件,後者告訴你錯誤發生在哪個元件路徑上,是接錯誤回報服務時最有價值的欄位。它在 commit 階段執行,可以安全地做 side effect(例如發送日誌),這一點跟 getDerivedStateFromError 不同——後者在 render 階段執行,必須是純函式。

相關術語: error boundary (由它實作)

出處:第 10 段「全域錯誤邊界:卡關與正解」

class component

類別元件

以 ES class 撰寫、繼承 React.Component 並實作 render 方法的元件寫法。

React 官方沒有計畫移除 class 元件,但新文件已全面改用函式元件,且 hooks 不能在 class 裡使用。今天還必須寫 class 的情境剩下唯一一個:error boundary。這正是這場面試裡「只用過函式元件」會踩到的那條線。

相關術語: error boundary (唯一必要用途)

出處:第 10 段「全域錯誤邊界:卡關與正解」

見仁見智 15:34
「But, in functional based component, we do not have any community we can say」
面試官這句(語音辨識疑為 equivalent)意思是「函式元件沒有等價寫法」。就 React 原生 API 而言正確,但實務上函式元件完全能用 error boundary——透過 react-error-boundary 套件,或 Next.js App Router 的 error.tsx;只是底層仍是 class 實作。說「函式元件無法做錯誤邊界」會誤導。
依據: react-error-boundary 套件;Next.js App Router error.tsx 慣例
留給下一段 這一題受試者卡住了,面試官給完答案就換題。他接下來挑的是幾個受試者一定答得出來的優化題,用意是在進入實作前把節奏調回來——而這些題目共享同一個想法:不要一次把全部東西交出去。

11. 一萬筆列表與 code splitting 15:40–17:45

兩題共用同一個想法:不要一次把全部東西交出去。列表題答題者提出 virtual list——只渲染使用者看得到的項目,捲動時再把其他項目掛上 DOM;面試官追問函式庫,答案是 react-window。code splitting 題他用五、六頁的應用舉例:使用者只進 settings 頁時,就只送該頁需要的程式碼,面試官順勢把它歸結為「降低 bundle 大小」。

承上 承上:上一段受試者卡關,面試官換上幾題他一定答得出來的優化題把節奏調回來。這兩題共享同一個想法,也正好呼應第 1 段開場時他自己列出的效能清單。

推理因為要在進入實作前確認基本盤穩固,面試官連問兩題「怎麼不要一次全給」。列表題受試者答 virtual list——只渲染使用者視窗內看得到的項目,捲動時再把後續項目掛上 DOM,把一萬個節點壓到十幾個;面試官追問函式庫,受試者猜了「React Virtual DOM」,面試官補正為 react-window。code splitting 題他用五、六頁的應用舉例:使用者只進 settings 頁時就只送該頁需要的程式碼,不必把全部頁面的程式一起下載;面試官順勢把它歸結成一句「就是降低 bundle 大小」。

AI 補充兩題的答案都對,但可以各補一層。虛擬列表最難的不是「只渲染看得到的」,而是撐出正確的捲軸高度與處理高度不固定的項目——項目高度不一時要邊捲邊量測、動態修正,這是 TanStack Virtual 這類函式庫的主要價值。另外現在有純 CSS 的輕量方案 content-visibility: auto,適合結構簡單的長列表。至於 code splitting,受試者說得像是伺服器有選擇性地送——實際上是 bundler 在打包時就把程式切成多個 chunk,瀏覽器依路由或 dynamic import 去要對應的檔案。這正好接回第 1 段他自己提過的 dynamic import:那是寫在原始碼裡的切點,code splitting 是它在打包層的結果。要注意切得太碎反而變慢,因為每個 chunk 都是一次額外的網路往返。

術語:virtualization

virtualization

列表虛擬化

只把使用者視窗內看得到的列表項目渲染成 DOM 節點,其餘用空白高度撐出捲軸。

又叫 windowing。三個實作難點:算出可視範圍對應的索引、撐出正確的總高度讓捲軸行為正常、以及處理高度不固定的項目(需要邊捲邊量測)。副作用是瀏覽器的 Ctrl+F 找不到未渲染的內容,無障礙軟體也可能讀不到,需要額外處理。主流套件是 react-window、react-virtuoso 與 TanStack Virtual。

相關術語: DOM (減少其節點)、code splitting (同一種思路)

出處:第 11 段「一萬筆列表與 code splitting」

code splitting

程式碼分割

把應用程式的程式碼切成多個獨立檔案,只在需要時才載入對應的部分。

最常見的切法是依路由切——每一頁一個 chunk。第二層是依元件切,用 dynamic import 把重元件(圖表、富文字編輯器、地圖)延後。Next.js 與多數框架的路由層切分是預設開啟的,所以實務上要手動處理的通常只有第二層。切分要適度:chunk 太多會讓瀏覽器發出大量小請求,反而拖慢。

相關術語: dynamic import (實作方式)、bundle (把它切開)

出處:第 11 段「一萬筆列表與 code splitting」

確定錯誤/已過時 16:32
「There are a bunch of libraries that we can we can use like React Virtual DOM is there」
沒有名為「React Virtual DOM」的虛擬列表函式庫——virtual DOM 是 React 的內部機制(見第 2 段),跟列表虛擬化是兩件事。受試者應該是把兩個名詞混在一起了。實際的套件是 react-window、react-virtualized、react-virtuoso 與 TanStack Virtual,面試官當場補正為 react-window。
依據: react-window / react-virtualized 皆為 Brian Vaughn 所作;npm 上無 react-virtual-dom 列表套件
留給下一段 這兩題都在減少「一次送出去的量」,但都還是客戶端主動去要資料。最後一題觀念題把方向反過來:如果需要伺服器主動推資料給你呢?

12. WebSocket 與請求/回應模型的差別 17:45–18:48

最後一題觀念題。答題者以 HTTP 的「一個 request 對一個 response」作對照:WebSocket 是先建立連線、監聽某個 room,之後送進該 room 的資料會持續推給客戶端,不需要客戶端不斷發請求。面試官複述為「即時資料傳輸」,答題者補上股票即時報價的使用情境。

承上 承上:上一段的兩個優化都建立在「客戶端主動去要、伺服器被動回應」這個前提上。這一段面試官把這個前提本身拿掉,問受試者伺服器主動推資料的情況。

推理因為前面所有題目都預設 HTTP 的請求/回應模型,面試官用最後一題觀念題檢查受試者知不知道還有別的模型。受試者的推導從對照開始:HTTP 是一個 request 對一個 response;WebSocket 則是先建立連線、開始監聽某些 room,之後只要有資料送進那個 room 就會持續推給客戶端,客戶端不需要一直發請求。面試官把它複述成「即時資料傳輸」,受試者補上股票即時報價當使用情境。

AI 補充描述正確,但「room」其實不是 WebSocket 本身的概念——那是 Socket.IO 這類函式庫在協定之上加的抽象。原生 WebSocket 只給你一條雙向管線,房間、自動重連、斷線緩衝都要自己或靠函式庫實作。另外面試官沒追問、但資深職位通常會考的是選型:只需要伺服器單向推播(通知、進度條、AI 逐字輸出)時,Server-Sent Events 更簡單——它走 HTTP,瀏覽器內建自動重連,也不需要另一套基礎設施。真正需要 WebSocket 的是雙向且高頻的場景:協作編輯、多人遊戲、聊天室。至於受試者舉的股票報價,兩者都可行,取決於客戶端要不要送東西回去。還有一個常被忽略的實務點——WebSocket 是長連線,伺服器端要處理連線數上限、心跳偵測與水平擴展時的訊息廣播,成本比看起來高。

術語:polling

WebSocket

網頁通訊端

在單一 TCP 連線上建立的全雙工通訊協定,建立後雙方都能主動送訊息。

透過 HTTP 的 Upgrade 標頭握手升級而來,之後就脫離請求/回應模型。原生 API 只提供連線與收送訊息,房間、重連、序列化都要自己處理,所以實務上常用 Socket.IO 或現在的 Pusher / Ably 這類託管服務。要注意長連線在水平擴展時的難題:使用者連到哪一台伺服器就綁在那台,跨機器廣播需要 Redis 之類的中介。

相關術語: Server-Sent Events (單向的替代)、polling (取代對象)

出處:第 12 段「WebSocket 與請求/回應模型的差別」

Server-Sent Events

伺服器推送事件

伺服器透過一條持續開著的 HTTP 連線,單向持續推送訊息給瀏覽器的機制。

瀏覽器端用 EventSource API,內建自動重連與事件 id 續傳。因為走一般 HTTP,可以直接通過既有的代理、CDN 與認證機制,部署成本遠低於 WebSocket。缺點是單向、且在 HTTP/1.1 下受同網域連線數限制。ChatGPT 這類逐字輸出的串流回應用的就是 SSE。

相關術語: WebSocket (更簡單的選項)

出處:第 12 段「WebSocket 與請求/回應模型的差別」

polling

輪詢

客戶端定時重複發送請求以檢查伺服器是否有新資料。

最土但最可靠的即時方案,缺點是延遲與空轉的請求量。改良版是 long polling:伺服器收到請求後不立刻回應,等到有資料或逾時才回,藉此把延遲壓到接近即時。在資料更新頻率低、或無法架設長連線基礎設施時,polling 仍然是合理選擇。

相關術語: WebSocket (被它取代)

出處:第 12 段「WebSocket 與請求/回應模型的差別」

留給下一段 到這裡所有觀念題都問完了,受試者除了 error boundary 之外幾乎全數答對,包含第 7 段那個 useRef 的定義。面試官接著要驗收的是最後也最關鍵的一件事:這些答得出來的觀念,手寫得出來嗎?

13. 實作題開場:碼表的三個要求 18:48–21:09

進入螢幕分享的實作題。面試官示範需求:兩顆按鈕,按 start 每秒累加,按 stop 立即停住,再按 start 從停下的數字接著跑,而且連按多次 start 也不能加速。答題者打開本機編輯器(提到公司目前用 Claude Code),先用 setInterval 寫出第一版,面試官提醒先把 CTA 做成按鈕、外觀不用管。

面試官在瀏覽器裡示範預期行為:start/stop 兩顆按鈕與跳動的秒數,這是整題的驗收標準
19:06 · 面試官在瀏覽器裡示範預期行為:start/stop 兩顆按鈕與跳動的秒數,這是整題的驗收標準
答題者的第一版程式碼,後面所有修正都是從這個版本長出來的
21:05 · 答題者的第一版程式碼,後面所有修正都是從這個版本長出來的
承上 承上:觀念題全部問完,受試者的定義幾乎都答對了。面試官現在要驗收的是「答得出來」與「寫得出來」之間的落差,於是要求分享螢幕,出了一道看起來只要五行的實作題。

推理因為前面的問答已經證明受試者的知識廣度足夠,面試官改測執行力。他先在瀏覽器裡示範預期行為,把需求壓縮成三條可驗收的規則:按 start 每秒累加一次;按 stop 立刻停住;再按 start 從停下的數字接著跑。示範完他追加了第四條,也是這題真正的考點——連按多次 start 不能讓數字加速。受試者打開本機的 Cursor 編輯器(順帶提到公司目前用的是 Claude Code),用 useState 存 count、用另一個 isCounting 狀態當開關,在 useEffect 裡依 isCounting 呼叫 setInterval。面試官只提醒把 CTA 做成按鈕、外觀不用管。

AI 補充從截圖可以看出第一版的結構已經埋好了後面所有的坑。程式碼是:useEffect 監看 isCounting,為真時呼叫 setInterval 每秒 setCount(prev => prev + 1);兩顆按鈕分別把 isCounting 設成 true 與 false。setCount 用函式形式是對的(呼應第 7 段提過的批次更新),但有兩個結構性問題。第一,setInterval 回傳的計時器 id 沒有被接住——它只存在於那次 effect 的 closure 裡(見第 5 段),一旦元件因為 count 改變而 re-render,就再也沒有人握著那個 id 可以清掉。第二,effect 沒有回傳清理函式,所以 isCounting 每次變動都會疊加一個新的 interval,舊的照跑。值得注意的是,這一題的正解正是他在第 7 段親口說對的那句話——「useRef 用來記住跨 re-render 仍然存在的值」,而計時器 id 正是這種值。

setInterval

定時重複執行

每隔指定毫秒重複執行一次 callback,回傳一個用來取消它的計時器 id。

兩個常被忽略的性質:一是它不保證準時,callback 只是被排進佇列,主執行緒忙碌時會延後,長時間跑會累積可觀的誤差,所以真正的碼表應該用 Date.now() 的差值而不是累加次數;二是它會一直跑到被明確取消為止,元件卸載時忘了清就是典型的記憶體洩漏。在 React 裡它必須跟 useRef 與清理函式一起使用。

相關術語: clearInterval (取消手段)、useRef (存放其 id)

出處:第 13 段「實作題開場:碼表的三個要求」

留給下一段 第一版的 setInterval id 沒有被存在任何跨 render 存活的地方。下一段面試官按下 stop,這個結構性問題立刻現形。

14. clearInterval 失效:提示改用 useRef 21:09–24:15

第一版的 stop 沒有作用。面試官指出問題在 clearInterval 拿不到正確的 interval id,並給提示:把 interval 存進 useRef 的 context 裡。答題者卡在「怎麼把值放進 ref」,以為 useRef 一定要綁在某個元素上;面試官說明可以直接把任意值存進 ref.current,停止時再從那裡拿來清掉。

面試官指出 stop 這裡清不掉 interval 的那段程式碼,是這段卡關的具體位置
22:19 · 面試官指出 stop 這裡清不掉 interval 的那段程式碼,是這段卡關的具體位置
承上 承上:第一版把 setInterval 的 id 丟在 effect 的 closure 裡,沒有存到任何跨 render 存活的地方。面試官按下 stop,問題立刻現形。

推理因為 stop 按鈕沒有作用,面試官直接指出病灶:這裡清不掉 interval,因為你手上沒有正確的計時器 id。他給的提示只有一句——把 interval 放進 useRef 的 context 裡,刻意不給寫法。受試者卡住的點不是不知道 useRef 是什麼(他在第 7 段才剛正確定義過),而是他認定 useRef 一定要綁在某個 DOM 元素上,於是不斷問「怎麼把值放進 ref」。面試官最後把那層誤解拆掉:ref 不需要綁任何元素,可以直接把任意值指派給 ref.current,停止時再從那裡取出來清掉。

AI 補充截圖裡看得到當時的中間狀態:stop 按鈕寫的是 onClick={() => clearInterval(1000)}——把延遲毫秒數當成計時器 id 傳了進去。這個寫法不會報錯,因為 clearInterval 收到不存在的 id 只會靜靜地什麼都不做,這正是這類 bug 難找的原因。同時 useEffect 的 else 分支裡還留著兩行註解掉的 clearInterval(interval),可以看出他原本想用區域變數接住 id,發現作用域不對之後就註解掉了——他其實摸到了正確方向,只差把那個變數換成 ref。這段暴露的是一個很典型的落差:受試者對 useRef 的記憶是「用來取得 DOM 元素」這個最常見的用例,而不是它的本質「一個跨 render 不變的容器」。判斷什麼該放進 ref 的標準只有一條——這個值變了,畫面需不需要跟著變?計時器 id 不需要,所以它屬於 ref。

clearInterval

取消定時器

用計時器 id 取消一個由 setInterval 建立的重複執行。

傳入不存在或錯誤的 id 不會拋錯,也不會有任何提示——這是它最容易讓人白找半天的地方。在 React 裡必須確保呼叫它時拿到的是「當前那一個」id,所以 id 要放在 useRef 裡,而不是 state(會延遲)或區域變數(會隨 render 消失)。清掉之後把 ref 設回 null 是好習慣,理由在後面會變成這題的關鍵。

相關術語: setInterval (配對使用)、useRef (存放其 id)

出處:第 14 段「clearInterval 失效:提示改用 useRef」

留給下一段 受試者拿到了「把 interval 存進 ref」這個方向,開始動手改寫。但他把 useRef 寫進了 useEffect 裡面,下一段編輯器立刻報錯。

15. hook 不能巢狀,與命名的建議 24:15–26:21

答題者把 useRef 寫進 useEffect 裡而報錯。面試官反問原因並揭曉:hook 不能放在另一個 hook 裡面。接著他建議這題根本不需要 useEffect,只要在 start/stop 事件裡處理即可,而且真實面試時應該把邏輯抽成獨立函式、取 startCounter / stopCounter 這類有意義的名字,而不是用一個共用函式靠參數分流。

在 useEffect 內呼叫 useRef 的錯誤畫面,對照 hook 規則才看得懂錯在哪
24:22 · 在 useEffect 內呼叫 useRef 的錯誤畫面,對照 hook 規則才看得懂錯在哪
承上 承上:受試者拿到「把 interval 存進 ref」的方向後開始改寫,卻把 useRef 寫進了 useEffect 內部。編輯器報錯,面試官接住這個錯誤當作下一個考點。

推理因為錯誤已經跳出來,面試官先反問受試者知不知道原因,確認他是照著提示改還是真的理解。受試者答不出來,面試官揭曉:不能在一個 hook 裡面呼叫另一個 hook。接著他做了一件更有價值的事——不只修錯,還修結構。他指出這題根本不需要 useEffect,只要在 start 與 stop 的事件處理器裡直接處理就好;並且建議真實面試時應該把邏輯抽成獨立函式,取 startCounter、stopCounter 這種有意義的名字,而不是像受試者打算的那樣寫一個共用函式、靠參數字串分流。

AI 補充面試官的說法要精確一點:Rules of Hooks 的規定是 hook 只能在元件函式或自訂 hook 的最頂層呼叫,不能寫在條件、迴圈或巢狀函式裡。useEffect 的 callback 就是一個巢狀函式,所以在裡面呼叫 useRef 違規。理由是 React 靠呼叫順序而不是名稱來對應每次 render 的 hook 狀態,順序一變就會錯位。從截圖可以看到當時的畫面:整個 useEffect 被註解掉了一大半,只剩 stopWatchRef = setInterval(...) 這行留在按鈕的 onClick 裡——注意它寫的是 stopWatchRef 而不是 stopWatchRef.current,這個遺漏會在下一段被面試官抓出來。至於「不需要 useEffect」這條建議,值得單獨記住:React 官方文件有一篇專門講「你可能不需要 effect」,判準是——這件事是回應使用者的操作而發生的嗎?是的話就寫在事件處理器裡;只有需要跟外部系統同步時才用 effect。碼表由按鈕觸發,屬於前者。

Rules of Hooks

Hook 使用規則

hook 只能在元件或自訂 hook 的最頂層呼叫,且不能寫在條件、迴圈或巢狀函式中。

根本原因是 React 用呼叫順序來對應每次 render 的 hook 狀態——它不知道你的變數叫什麼,只記得「第幾個 useState」。順序一旦改變,state 就會接錯到別的 hook 上。eslint-plugin-react-hooks 會靜態檢查這條規則,React 19 起某些違規也會在執行期直接報錯。另一條配套規則是:只能在 React 函式元件或以 use 開頭的自訂 hook 裡呼叫。

相關術語: useEffect (受其約束)、useRef (受其約束)

出處:第 15 段「hook 不能巢狀,與命名的建議」

留給下一段 受試者的 ref 寫成了 stopWatchRef 而不是 stopWatchRef.current。下一段面試官先補上這個 .current,然後請他連按四次 start——真正的考點終於登場。

16. 連按 start 的 bug 與錯誤方向 26:21–29:25

面試官先修正寫法:值要存進 stopwatchRef.current(第 12 行)。接著請答題者連按三四次 start,暴露出計數加速的 bug。答題者依序提出 debounce、加一個 hasStarted 狀態、useCallback 記憶化,面試官逐一擋掉並要求「不要多寫程式碼,只找出這個 bug」。

第 12 行加上 .current 的修正,是後續 bug 討論的程式碼基準
26:25 · 第 12 行加上 .current 的修正,是後續 bug 討論的程式碼基準
承上 承上:上一段留下一個沒接完的細節——ref 被寫成 stopWatchRef 而不是 stopWatchRef.current。這一段面試官先把它補上,然後啟動這題真正的考點。

推理因為結構已經修好,面試官先處理最後一個語法層面的問題:值必須存進 stopWatchRef.current,位置在第 12 行。改完之後他請受試者連按三、四次 start,數字立刻加速——第 13 段開場就宣告過的第四條要求現在被驗收了。受試者依序提出三個方向:先想到 debounce,自己隨即發現不行(間隔久一點的兩次點擊仍會各觸發一次);再提出加一個 hasStarted 狀態來擋;最後猜是要用 useCallback 記憶化。面試官逐一擋掉,並收緊題目——不要多寫任何程式碼,只要找出這個 bug。

AI 補充面試官擋掉這三個方向的理由值得拆開看,因為它們代表三種常見的思考偏差。debounce(第 4 段定義過)是在「事件太密集」時用的,但這裡的問題不是點擊太密集,而是每一次點擊都合法地又開了一個計時器——就算兩次點擊隔十秒,一樣會有兩個 interval 在跑。hasStarted 狀態方向是對的,但它是在原本就該存在的資訊之外,額外再造一個平行的真相來源;ref 裡有沒有值本來就已經代表「有沒有在跑」了,多一個 state 就多一個要同步的東西。useCallback 則完全無關——它只影響函式的參照是否穩定,不影響按鈕被按幾次。受試者自己也馬上意識到這點。從截圖可以看到此時程式碼已經重構成一個 updateCountLogic('start' | 'stop') 的共用函式,而編輯器正跳出 TypeScript 錯誤 Type 'Timeout' is not assignable to type 'null'——這其實是個線索:ref 的型別被宣告成可以是 null,但程式從來沒有把它設回 null 過。

useCallback

函式記憶化 hook

回傳一個記憶化的函式參照,依賴沒變時每次 render 都給同一個函式。

它記住的是函式本身,不是執行結果(那是 useMemo)。唯一有意義的用途是讓下游的 React.memo 元件或 useEffect 的依賴陣列不要因為函式每次都新建而失效——單獨使用不會有任何效能提升,反而多一次比較成本。React 19 的編譯器可以自動處理大部分這類記憶化,手寫 useCallback 的需求正在減少。

相關術語: memoization (一種)、dependency array (依賴其判定)

出處:第 16 段「連按 start 的 bug 與錯誤方向」

留給下一段 三個方向都被擋掉,而編輯器的型別錯誤已經把答案指出來了:ref 從來沒有被設回 null。下一段面試官揭曉這個 bug,並補上最後一塊拼圖。

17. 真正的修法:清掉 ref 並在啟動前檢查 29:25–30:36

面試官給出答案:stop 時只呼叫 clearInterval,ref 裡仍留著舊值,所以要一併把 ref 設為 null(第 16 行)。改完仍不動,因為 start 還缺一個守門條件——只有在 ref.current 沒有值時才啟動 interval,加上否定判斷後功能正常。面試官宣布面試結束,答題者只要求回饋。

stop 裡把 ref 設成 null 的那兩行,是修正的第一半
29:40 · stop 裡把 ref 設成 null 的那兩行,是修正的第一半
加上 if (!ref.current) 之後碼表正常運作的最終畫面
30:28 · 加上 if (!ref.current) 之後碼表正常運作的最終畫面
承上 承上:上一段三個方向都被擋掉,而編輯器的型別警告已經暗示了答案——ref 從來沒有被設回 null。這一段面試官把它挑明。

推理因為受試者始終在「加東西」而不是「看現有狀態」,面試官直接給出診斷:stop 只呼叫了 clearInterval,ref 裡仍留著舊的計時器 id,所以程式無從判斷現在到底在不在跑。修法是在 stop 時一併把 ref 設成 null(第 16 行)。改完仍然沒解決連按的問題,因為還缺另一半——面試官補上最後一塊:只有在 ref.current 沒有值的時候才啟動 interval。受試者先把判斷式寫反了,加上否定之後碼表終於符合全部四條要求。面試官宣布面試結束,受試者只要求回饋。

AI 補充把兩塊拼起來,這題的完整解法只有三行的形狀:start 時先檢查 if (!ref.current) 才 setInterval 並把 id 存進 ref.current;stop 時 clearInterval(ref.current) 之後把 ref.current 設回 null。ref 在這裡同時扮演兩個角色——既是計時器 id 的保管處,也是「現在有沒有在跑」這個狀態本身。這正是第 16 段裡 hasStarted 那個方向被擋掉的理由:那個資訊本來就已經存在,只是沒被清乾淨所以不可信。從兩張截圖的對照可以看出修好的過程:第一張 count 停在 39、按鈕沒反應,第二張 count 從 5 開始正常累加。這一段也回頭驗證了整場面試的推論——受試者在第 7 段就正確說出「useRef 用來記住跨 re-render 存在的值」,在第 14 段拿到了「把 interval 存進 ref」的明示提示,卻仍需要面試官逐行帶著走完。知道一個 API 的定義,和知道它在一個具體問題裡該承擔什麼角色,中間隔著的正是這場面試想量的距離。

術語:single source of truth

single source of truth

單一真相來源

同一項資訊只在一個地方保存,其他需要它的地方都由該處推導。

這題的正解就是它的最小示範:ref.current 有沒有值,同時回答了「計時器 id 是多少」與「現在在不在跑」。若另外再開一個 hasStarted 狀態,就有兩個地方各自記著同一件事,任何一邊忘記更新就會不一致——而這正是原本的 bug(清了 interval 卻沒清 ref)的放大版。實務上的判準是:這個 state 能不能從其他既有的 state 推導出來?可以的話就不該存在。

相關術語: useRef (此題的載體)、useState (避免其冗餘)

出處:第 17 段「真正的修法:清掉 ref 並在啟動前檢查」

留給下一段 總結收束。

4. 總結

面試官用一條由外而內、再由知到行的推理鏈組織了整場面試。他先用最開放的效能題讓受試者自己攤開知識地圖(第 1 段),從受試者的回答裡抓出 tree shaking 追問,確認名詞不是背的。接著他把層級一路往下推:React 靠 reconciliation 決定要改哪些 DOM(第 2 段),DOM 改完後瀏覽器還要走完整條 critical rendering path 才會出現畫面(第 3 段)。既然每次更新都這麼貴,開發者能做的第一件事就是少觸發幾次,於是問 debounce 與 throttle(第 4 段);而 debounce 要在延遲後還記得參數,靠的是 closure(第 5 段),順勢帶出唯一不由出生位置決定的 this(第 6 段)。地基確認後回到 React,用同型的對照題問 useRef 與 useState(第 7 段)——受試者正確答出「useRef 記住跨 re-render 的值」,這句話會在二十分鐘後被逐字驗收。往回追一層 hooks 取代了什麼(第 8 段),受試者說出「只用過函式元件」,面試官記下這條線索,先讓他答完擅長的 SSR(第 9 段),再兌現成陷阱:global error boundary 至今只有 class 元件的 componentDidCatch 有原生解(第 10 段),受試者卡住。面試官給完答案就用兩題「不要一次全給」的優化題調回節奏(第 11 段),再用 WebSocket 把請求/回應模型反過來收尾(第 12 段)。觀念題全數問完之後,真正的考點才登場:一道只要三行的 stopwatch。受試者依序踩進四個坑——interval id 存在 closure 裡所以清不掉(第 13、14 段)、把 useRef 寫進 useEffect 違反 Rules of Hooks(第 15 段)、連按 start 時往 debounce 與 useCallback 找錯方向(第 16 段),最後由面試官揭曉正解:stop 時要把 ref 設回 null,start 前要檢查 ref.current 是否為空(第 17 段)。整條鏈的結論是:ref 同時是計時器 id 的保管處和「有沒有在跑」的單一真相來源,而受試者在第 7 段就已經正確定義過這個工具——資深的門檻不在定義,在於能不能把定義用在具體問題上。

勘誤總整理

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

段落原話(transcript 逐字)說明
8. 生命週期方法到 useEffect
10:25
「I think in it's React 13, hooks was introduced」hooks 不是在 React 13 引入的,而是 React 16.8(2019 年 2 月正式釋出)。React 也從未有過 13 這個版本號——16 之後直接跳到 17、18、19。
依據: React 16.8 release notes, 2019-02-06
11. 一萬筆列表與 code splitting
16:32
「There are a bunch of libraries that we can we can use like React Virtual DOM is there」沒有名為「React Virtual DOM」的虛擬列表函式庫——virtual DOM 是 React 的內部機制(見第 2 段),跟列表虛擬化是兩件事。受試者應該是把兩個名詞混在一起了。實際的套件是 react-window、react-virtualized、react-virtuoso 與 TanStack Virtual,面試官當場補正為 react-window。
依據: react-window / react-virtualized 皆為 Brian Vaughn 所作;npm 上無 react-virtual-dom 列表套件
8. 生命週期方法到 useEffect
10:40
「If the dependency array is empty, whatever is there in the use effect will fire when the component is unmounted」依賴陣列為空時,effect 內容是在元件「掛載後」執行一次,不是卸載時;卸載時執行的是 effect 回傳的清理函式。受試者下一句就正確描述了清理函式,可見是口誤,但字面說法會誤導。
依據: React 官方文件 useEffect:Reference / Parameters
9. SSR 為什麼快,又為什麼利於 SEO
12:54
「First input delay and it will make it better」兩個問題:一是 FID(First Input Delay)已於 2024 年 3 月被 INP(Interaction to Next Paint)取代,不再是 Core Web Vitals 指標;二是 SSR 不必然改善互動延遲——HTML 提早出現但 hydration 未完成時,使用者的點擊反而可能等更久。
依據: Google web.dev, INP replaced FID as a Core Web Vital in March 2024
10. 全域錯誤邊界:卡關與正解
15:34
「But, in functional based component, we do not have any community we can say」面試官這句(語音辨識疑為 equivalent)意思是「函式元件沒有等價寫法」。就 React 原生 API 而言正確,但實務上函式元件完全能用 error boundary——透過 react-error-boundary 套件,或 Next.js App Router 的 error.tsx;只是底層仍是 class 實作。說「函式元件無法做錯誤邊界」會誤導。
依據: react-error-boundary 套件;Next.js App Router error.tsx 慣例

5. 推薦三個下一步

1. 往下挖深:React 的 Rules of Hooks 與 Fiber 內部機制

第 15 段面試官只說了「hook 不能放在 hook 裡」,但沒說為什麼。理解 React 靠呼叫順序而非名稱對應 hook 狀態,才會知道這條規則不是約定俗成而是實作的必然,也才能看懂 reconciliation 在 Fiber 架構下如何被中斷與恢復。

YouTube 搜尋:React Fiber architecture explained why rules of hooks exist React internals React hooks linked list implementation

2. 往旁邊對照:React 官方的「你可能不需要 useEffect」

第 15 段面試官指出這題根本不需要 useEffect,只要寫在事件處理器裡——這是整場面試裡最有價值卻最沒展開的一句建議。把「回應使用者操作」與「跟外部系統同步」這兩類情境分清楚,能一次解掉大量 effect 相關的 bug,包含第 8 段提到的 stale closure。

YouTube 搜尋:You Might Not Need an Effect React useEffect anti patterns React derive state instead of useEffect

3. 往上應用:把同型的計時器題寫成可重用的自訂 hook

這題的正解形狀(ref 存 id + 啟動前檢查 + 停止時清空)在 debounce、throttle、輪詢、動畫迴圈裡是同一套。把它抽成 useInterval 這類自訂 hook,等於把第 4、13、14、17 段的內容一次串起來,也是前端面試常見的下一階段考題。

YouTube 搜尋:useInterval custom hook Dan Abramov React custom hooks interview questions useTimeout useDebounce implementation React

📄 全部影片 · 主題區: 前端底層與系統設計