後面我們會用「閉包保存狀態」的方式來寫 signal()
,並以「物件解構」來取值與改值(const { get, set } = signal(0)
),不了解這兩者,很容易在教學中出現「值被快照、反應式失效」或「this 綁定錯誤」的誤解。
不要覺得不可能,我之前在某知名企業下工作,就遇過該司前端的技術主管,居然對解構賦值這種簡單的概念有錯誤認知,也可能經驗太少,又只有接觸過 React 這個框架。
所以還是以防萬一,我們稍微複習一下再開始實作(已經具備此觀念的朋友可以跳過此篇)。
讓函式可以「記住」它被建立當下的詞法作用域中的變數,哪怕離開了那個作用域,還是能讀寫它。
詳細的介紹可以參考我的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
value
本體,只能透過 get
/set
;這和 class 的 private 、或 Proxy
封裝是同一種意圖。get
/set
本身就是穩定函式(不需要每次重建),非常適合之後和 React、事件處理、或任何 callback 整合。useState
:signal()
任何時候都能建立,框架中立。閉包不是魔法,它只是函數 + 詞法作用域。理解這點,後面談追蹤依賴、scheduler 都會更自然。
陣列或物件可以被「拆解」到多個變數中,並支援重新命名、預設值與巢狀解構。
這就是我上述親身經歷的體驗,但我想應該也很多人不清楚這個概念的細節,如果想了解更多可以參考這篇文章。
我們直接以 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
get
/set
/peek
/on
…),比位置敏感的陣列解構可讀性更高,也更容易長成可發現的 API。const value = get()
然後傳來傳去嗎?
get
這個函數本身,或在用到時再 get()
。get()
的那一刻。this
綁定問題;也便於 tree-shaking 與函數式組合。綜合上述概念的結果,也是之後概念的延伸:
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 的基礎。
下一篇,我們會依照這個基礎加入訂閱機制的實作。