iT邦幫忙

2025 iThome 鐵人賽

DAY 9
1

為什麼需要這篇?

後面我們會用「閉包保存狀態」的方式來寫 signal(),並以「物件解構」來取值與改值(const { get, set } = signal(0)),不了解這兩者,很容易在教學中出現「值被快照、反應式失效」或「this 綁定錯誤」的誤解。

不要覺得不可能,我之前在某知名企業下工作,就遇過該司前端的技術主管,居然對解構賦值這種簡單的概念有錯誤認知,也可能經驗太少,又只有接觸過 React 這個框架。

所以還是以防萬一,我們稍微複習一下再開始實作(已經具備此觀念的朋友可以跳過此篇)。

閉包(Closure)

讓函式可以「記住」它被建立當下的詞法作用域中的變數,哪怕離開了那個作用域,還是能讀寫它。

詳細的介紹可以參考我的Medium文章,如果看不懂作用域的話可以參考這篇
我們以文章中的範例,修改成 Signal 應該有的樣子

function signal<T>(initial: T) {
    // 被閉包「記住」的私有狀態
    let value = initial;
    // 讀取
    const get = () => value;
    // 更新
    const set = (next: T) => { value = next };
    // 物件回傳的原因是會讓大家比較好看懂,如果你喜歡也可以是陣列格式
    return { get, set };
}

const count = signal(0);
count.set(count.get() + 1);
console.log(count.get()); // 1

這樣做的好處

  • 私有性(immutable):
    外部拿不到 value 本體,只能透過 get/set;這和 class 的 private 、或 Proxy 封裝是同一種意圖。
  • 穩定引用:
    get/set 本身就是穩定函式(不需要每次重建),非常適合之後和 React、事件處理、或任何 callback 整合。
  • 對比 useState
    React Hook 需要在 render 內呼叫;閉包式 signal() 任何時候都能建立,框架中立。

閉包不是魔法,它只是函數 + 詞法作用域。理解這點,後面談追蹤依賴、scheduler 都會更自然。

解構賦值(Destructuring)

陣列或物件可以被「拆解」到多個變數中,並支援重新命名、預設值與巢狀解構。

這就是我上述親身經歷的體驗,但我想應該也很多人不清楚這個概念的細節,如果想了解更多可以參考這篇文章

陣列解構

我們直接以 Solid 的範例來理解

function createSignal<T>(initial: T) {
    let value = initial;
    const getter = () => value;
    const setter = (next: T) => { value = next };
    return [getter, setter] as const;
}

const [count, setCount] = createSignal(0);
setCount(count() + 1);

物件解構

以我們上面的範例來理解

function signal<T>(initial: T) {
    let value = initial;
    const get = () => value;
    const set = (next: T) => { value = next };
    return { get, set };
}

const { get, set } = signal(0);
set(get() + 1);

如果要改名也是可以:

function signal<T>(initial: T) {
    let value = initial;
    const get = () => value;
    const set = (next: T) => { value = next };
    return { get, set };
}

const { get: count, set: setCount } = signal(0);
setCount(count() + 1);

常見誤區

  • 解構的是「參照」不是「拷貝」

    • const { get } = signal(0) 取到的是函數參照(refference),不是當下值快照。只有你呼叫 get()才會讀取到「目前」的狀態。
  • 不要把值先取出就一直用(賦值概念錯誤)

    • 要最新值就再呼叫 get(),不要保存舊值。
const { get, set } = signal(0);
const v = get();  // pass by value, as a snapshot
set(10);
console.log(v);   // 仍是 0!不是 10

為什麼 Signal 喜歡「閉包 + 解構」

  • 閉包讓內部狀態私有、可控,天然支援「計算快取」「相等性比較」「訂閱名單」等擴充。
  • 物件解構讓教學語句清晰(get/set/peek/on…),比位置敏感的陣列解構可讀性更高,也更容易長成可發現的 API。

常見問題

  • 我可以 const value = get() 然後傳來傳去嗎?
    • 可以,但那是快照;如果要最新狀態,請傳 get 這個函數本身,或在用到時再 get()
  • 解構之後會不會破壞反應式?
    • 不會,因為我們回傳的是方法,而非值;反應式的「讀取點」發生在你呼叫 get() 的那一刻。
  • 為什麼不用 class?
    • 閉包更輕量、避免 this 綁定問題;也便於 tree-shaking 與函數式組合。

回傳物件格式的 Signal

綜合上述概念的結果,也是之後概念的延伸:

export type Signal<T> = {
    get(): T;
    set(next: T | ((prev: T) => T)): void;
};

export function signal<T>(initial: T): Signal<T> {
    let value = initial;
    const get = () => value;
    const set = (next: T | ((p: T) => T)) => {
        const nxtVal = typeof next === 'function' ? (next as (p: T) => T)(value) : next;
        const isEqual = Object.is(value, nxtVal);
        if (!isEqual) value = nxtVal;
    };
    return { get, set };
}

結語

這篇是複習一下 JavaScript 的基本概念,還有如何透過這兩個概念實作出 Signal 的基礎。
下一篇,我們會依照這個基礎加入訂閱機制的實作。


上一篇
Dependency Tracking 基本原理(II)
系列文
Reactivity 小技巧大變革:掌握 Signals 就這麼簡單!9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言