JS 實戰 (5) —— Signals:現代響應式之巔與細粒度更新
在系列文章的第一篇中,我們學會了如何用 get / set 實現響應式。隨著技術演進,現代前端界出現了一個橫掃所有主流框架的新寵兒:Signals。
從 Solid.js 的崛起,到 Preact 的引入,再到 Vue 3.4 的底層重構與 Angular 17+ 的全面擁抱——我們正處於「Signals 時代」。
一、 什麼是 Signal?
簡單來說,Signal 是一個同時具備「值」與「通知能力」的容器。
1. 語法初探
在多數庫中,它的長相像這樣:
javascript
// 偽代碼
const count = signal(0);
// 讀取:它是一個函式,或者透過 .value 觸發 Getter
console.log(count());
// 寫入:透過函式呼叫或 Setter
count.set(count() + 1);二、 為什麼需要 Signals?(vs Virtual DOM)
React 引領了 Virtual DOM 的時代:只要資料變了,我就重新執行整個元件函式,產生一棵新的虛擬樹,然後進行比對 (Diffing)。
Signals 的哲學完全不同:細粒度 (Fine-grained)。
- 直達末梢:當
count改變時,Signal 知道到底在哪個 HTML 標籤裡讀取了它。 - 無需 Diff:它不需要重新執行整個元件,也不需要比對整棵樹,它可以精準地只更新那個變動的文本節點或屬性。
三、 底層機制:Getter 的終極應用
Signals 的魔法關鍵在於:自動依賴追蹤。而這正是利用了我們學過的 Getter 機制。
javascript
// 一個極簡版的 Signal 實現
let activeEffect = null;
function signal(initialValue) {
let _val = initialValue;
const subscribers = new Set();
return {
get value() {
// 核心:誰在讀取我?我就把誰存起來 (依賴收集)
if (activeEffect) subscribers.add(activeEffect);
return _val;
},
set value(newVal) {
if (_val === newVal) return;
_val = newVal;
// 核心:資料變了,通知所有人執行 (派發更新)
subscribers.forEach((fn) => fn());
},
};
}
function effect(fn) {
activeEffect = fn;
fn(); // 第一次執行,觸發內部 Signal 的 Getter
activeEffect = null;
}四、 Signal 與傳統 Ref/Reactive 的區別
在 Vue 3 中,ref 跟 signal 其實非常接近,但 Signals 通常具備以下進階特性:
- 延遲執行 (Lazy Execution):只有在屬性真正需要被讀取時,才會進行計算。
- 無縫組合 (Composability):你可以輕鬆地把多個 Signals 組合成一個
computedSignal。 - 框架無關:現在的
Signals規範正朝向標準化邁進,許多庫甚至可以脫離 UI 框架獨立運行。
五、 實戰場景:什麼時候該用?
- 高頻更新:例如股票報價圖表、大型複雜表格,不適合 Virtual DOM 全量比對的場景。
- 超輕量組件:像 Solid.js 這種追求零開銷 (Zero-overhead) 的原生開發體驗。
- 跨組件狀態同步:Signals 天生就適合在不同的元件樹分支間同步狀態。
總結
JavaScript 的演進始終圍繞著「如何更有效率地處理狀態變動」。從基礎的物件屬性,到 defineProperty 的劫持,再到 Proxy 的代理,最後演變成了 Signals 這種細粒度的原語。
理解了 Accessors,你就理解了這一切的根基。
️ 進階挑戰
- 實作挑戰:試著用上面的
signal程式碼實作一個computed(fn)。它應該能在依賴的 Signal 變動時,自動重新計算結果。 - 深度思考:如果 Signals 這麼好,為什麼 React 官方至今仍不考慮官方支援 Signals?(提示:React 的純函式渲染模型與 Signals 的可變/依賴收集模型有什麼衝突?)。
延伸閱讀與資源
- Solid.js Docs: Reactivity Concepts
- Preact Signals: Official Library
- Engineering Blog: The Evolution of Signals