solid.js響應式createSignal 源碼解析
正文
createSignal
用來創(chuàng)建響應式數(shù)據(jù),它可以跟蹤單個值的變化。
solid.js 的響應式實現(xiàn)參考了 S.js,它是一個體積超小的 reactive 庫,支持自動收集依賴和簡單的響應式編程。
createSignal
首先我們來看下 createSignal
的聲明:
// packages/solid/src/reactive/signal.ts export interface BaseOptions { name?: string; } export interface EffectOptions extends BaseOptions {} export interface MemoOptions<T> extends EffectOptions { equals?: false | ((prev: T, next: T) => boolean); } export type Accessor<T> = () => T; export type Setter<T> = (undefined extends T ? () => undefined : {}) & (<U extends T>(value: (prev: T) => U) => U) & (<U extends T>(value: Exclude<U, Function>) => U) & (<U extends T>(value: Exclude<U, Function> | ((prev: T) => U)) => U);
// packages/solid/src/reactive/signal.ts export type Signal<T> = [get: Accessor<T>, set: Setter<T>]; export interface SignalOptions<T> extends MemoOptions<T> { internal?: boolean; } export function createSignal<T>(): Signal<T | undefined>; export function createSignal<T>(value: T, options?: SignalOptions<T>): Signal<T>;
可以看到 createSignal
支持兩個參數(shù),分別是 value 和 options,然后返回一個包含 setter 和 getter 的數(shù)組。
參數(shù):
- value:初始值,默認值為
undefiend
- options
- equals:自定義比較器,用于新舊值比較或觸發(fā)強制更新,允許傳遞函數(shù)或者 false;
- internal(可選):標識是否為內置屬性,應用于開發(fā)環(huán)境,生產環(huán)境會移除掉相關邏輯;
- name(可選):自定義屬性對象名稱,應用于開發(fā)環(huán)境,生產環(huán)境會移除掉相關邏輯。
返回值:
- getter:返回當前值,以函數(shù)形式調用
- 自動進行依賴收集。例如在
createEffect
中調用 getter, state 對象會與 effect 建立依賴關系。
- 自動進行依賴收集。例如在
- setter:設置值,以函數(shù)形式調用
- 如果存在依賴當前 state 對象的觀察者,循環(huán)執(zhí)行觀察者數(shù)組。
了解 createSignal
聲明之后,下面我們來看下具體實現(xiàn)。
// packages/solid/src/reactive/signal.ts export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> { options = options ? Object.assign({}, signalOptions, options) : signalOptions; const s: SignalState<T> = { value, observers: null, observerSlots: null, comparator: options.equals || undefined }; if ("_SOLID_DEV_" && !options.internal) s.name = registerGraph(options.name || hashValue(value), s as { value: unknown }); const setter: Setter<T | undefined> = (value?: unknown) => { if (typeof value === "function") { if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue); else value = value(s.value); } return writeSignal(s, value); }; return [readSignal.bind(s), setter]; }
如果用戶傳入 options,會對 options 和默認的 options 進行合并,否則使用默認 options。
// packages/solid/src/reactive/signal.ts export const equalFn = <T>(a: T, b: T) => a === b; const signalOptions = { equals: equalFn };
默認配置只有一個 equals
屬性,值為 equalFn
,用于比較兩個值是否相同。
由于這里比較的是引用地址,所以當你改變一個對象的某個屬性,重新賦值時,相關訂閱并不會被觸發(fā),所以這時我們可以在傳入的 options 配置中配置 equals
為 false 或者自定義其他比較邏輯。
例如下面的案例:
const [object, setObject] = createSignal({ count: 0 }); createEffect(() => { console.log(object()); }); object().count = 2; setObject(object); setObject(current => { current.count += 1; current.updated = new Date(); return current; }); // { count: 0 }
上述代碼在運行時 effect 中代碼只會觸發(fā)一次,這可能與我們的預期不符,所以我們可以傳入自定義 options。
const [object, setObject] = createSignal({ count: 0 }, { equals: false }); // { count: 0 } // { count: 2 } // { count: 3, updated: 2022-09-11T08:21:44.258Z }
當我們設置 equals 屬性為 false,effect 就會被觸發(fā) 3 次。
除此之外,我們還可以使用該配置作為觸發(fā)器來使用,這里就不展開闡述了。感興趣可以查看官方提供的案例,createSignal。
下面讓我們繼續(xù)查看代碼:
// packages/solid/src/reactive/signal.ts export interface SignalState<T> { value?: T; observers: Computation<any>[] | null; observerSlots: number[] | null; tValue?: T; comparator?: (prev: T, next: T) => boolean; name?: string; } export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> { options = options ? Object.assign({}, signalOptions, options) : signalOptions; const s: SignalState<T> = { value, observers: null, observerSlots: null, comparator: options.equals || undefined }; if ("_SOLID_DEV_" && !options.internal) s.name = registerGraph(options.name || hashValue(value), s as { value: unknown }); const setter: Setter<T | undefined> = (value?: unknown) => { if (typeof value === "function") { if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue); else value = value(s.value); } return writeSignal(s, value); }; return [readSignal.bind(s), setter]; }
在 createSignal
定義了 s
對象,它有四個屬性,分別是:
- value:傳入的值
- observers:觀察者數(shù)組
- observerSlots:觀察者對象在數(shù)組的位置
- comparator:比較器
// packages/solid/src/reactive/signal.ts if ("_SOLID_DEV_" && !options.internal) s.name = registerGraph(options.name || hashValue(value), s as { value: unknown });
這段代碼為 state 對象設置了 name 屬性,不過它只作用于開發(fā)環(huán)境,生產環(huán)境打包時 _SOLID_DEV_
變量會被替換為 false,然后會作為 decode 被移除掉。
// packages/solid/rollup.config.js export default [ { input: "src/index.ts", // ... plugins: [ replace({ '"_SOLID_DEV_"': false, preventAssignment: true, delimiters: ["", ""] }) ].concat(plugins) } ]
接下來定義 setter 函數(shù):首先會對 value 的值進行判斷,如果傳遞的 setter 是一個 函數(shù):
- 如果發(fā)現(xiàn)
Transition
存在,并且Transition.sources
中存在當前 state,會使用s.tValue
屬性值; - 如果上述條件不滿足,會使用當前 state 的 value 屬性值。
然后調用 wrtieSignal
,并返回其結果。
// packages/solid/src/reactive/signal.ts export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> { // ... const setter: Setter<T | undefined> = (value?: unknown) => { if (typeof value === "function") { if (Transition && Transition.running && Transition.sources.has(s)) value = value(s.tValue); else value = value(s.value); } return writeSignal(s, value); }; return [readSignal.bind(s), setter]; }
最后返回操作數(shù)組:第一個參數(shù)為 readSignal 函數(shù),用來返回 s 中的 value 值,第二個參數(shù)就是 setter。
總結一下,createSignal
首先會合并用戶 options,其次會定義 state 對象,用來記錄當前值和依賴關系,然后定義 setter 函數(shù),用來設置值,最后返回一個數(shù)組,分別是 readSignal
函數(shù)和 setter
函數(shù)。
readSignal
看完 createSignal 定義,接著我們再來看下 readSignal,這個方法非常重要。solid.js 依賴關系的建立就發(fā)生在這個方法中。
// packages/solid/src/reactive/signal.ts // Internal export function readSignal(this: SignalState<any> | Memo<any>) { const runningTransition = Transition && Transition.running; if ( (this as Memo<any>).sources && ((!runningTransition && (this as Memo<any>).state) || (runningTransition && (this as Memo<any>).tState)) ) { if ( (!runningTransition && (this as Memo<any>).state === STALE) || (runningTransition && (this as Memo<any>).tState === STALE) ) updateComputation(this as Memo<any>); else { const updates = Updates; Updates = null; runUpdates(() => lookUpstream(this as Memo<any>), false); Updates = updates; } } if (Listener) { const sSlot = this.observers ? this.observers.length : 0; if (!Listener.sources) { Listener.sources = [this]; Listener.sourceSlots = [sSlot]; } else { Listener.sources.push(this); Listener.sourceSlots!.push(sSlot); } if (!this.observers) { this.observers = [Listener]; this.observerSlots = [Listener.sources.length - 1]; } else { this.observers.push(Listener); this.observerSlots!.push(Listener.sources.length - 1); } } if (runningTransition && Transition!.sources.has(this)) return this.tValue; return this.value; }
函數(shù)內部首先判斷是否正在 transition,我們暫時不需要關心這段邏輯,直接跳到下面這段邏輯:
// packages/solid/src/reactive/signal.ts export function readSignal(this: SignalState<any> | Memo<any>) { // ... if (Listener) { const sSlot = this.observers ? this.observers.length : 0; if (!Listener.sources) { Listener.sources = [this]; Listener.sourceSlots = [sSlot]; } else { Listener.sources.push(this); Listener.sourceSlots!.push(sSlot); } if (!this.observers) { this.observers = [Listener]; this.observerSlots = [Listener.sources.length - 1]; } else { this.observers.push(Listener); this.observerSlots!.push(Listener.sources.length - 1); } } if (runningTransition && Transition!.sources.has(this)) return this.tValue; return this.value; }
首先會判斷 Listener 是否存在,如果存在才會執(zhí)行這段代碼。那么這個 Listener 是什么時候被定義并賦值的呢?
// packages/solid/src/reactive/signal.ts let Listener: Computation<any> | null = null; let Updates: Computation<any>[] | null = null; let Effects: Computation<any>[] | null = null;
Listener 是一個全局變量,默認值是 null。同時還定義了 Updates
、Effectes
數(shù)組,它們都是 Computation 類型。
export type EffectFunction<Prev, Next extends Prev = Prev> = (v: Prev) => Next; export interface SignalState<T> { value?: T; observers: Computation<any>[] | null; observerSlots: number[] | null; tValue?: T; comparator?: (prev: T, next: T) => boolean; name?: string; } export interface Owner { owned: Computation<any>[] | null; cleanups: (() => void)[] | null; owner: Owner | null; context: any | null; sourceMap?: Record<string, { value: unknown }>; name?: string; componentName?: string; } export interface Computation<Init, Next extends Init = Init> extends Owner { fn: EffectFunction<Init, Next>; state: number; tState?: number; sources: SignalState<Next>[] | null; sourceSlots: number[] | null; value?: Init; updatedAt: number | null; pure: boolean; user?: boolean; suspense?: SuspenseContextType; }
可以看到 Computation 是一個對象,定義了很多屬性,基本都不知道啥作用。不過其中一個 sources 屬性,你是否也感覺很眼熟?
對,它就是一個普通的 signal 對象,也就是我們調用 createSignal
方法時,內部創(chuàng)建的 s 對象。
另外可以看到,上面 SignalState
接口聲明中的 observers 就是一個 Computation 類型的數(shù)組,這時我們已經知道 state 和 computation 互相依賴,并且是多對多的關系。
接下來再回到代碼:
// packages/solid/src/reactive/signal.ts export function readSignal(this: SignalState<any> | Memo<any>) { // ... if (Listener) { const sSlot = this.observers ? this.observers.length : 0; if (!Listener.sources) { Listener.sources = [this]; Listener.sourceSlots = [sSlot]; } else { Listener.sources.push(this); Listener.sourceSlots!.push(sSlot); } if (!this.observers) { this.observers = [Listener]; this.observerSlots = [Listener.sources.length - 1]; } else { this.observers.push(Listener); this.observerSlots!.push(Listener.sources.length - 1); } } if (runningTransition && Transition!.sources.has(this)) return this.tValue; return this.value; }
當 Listener 存在時,首先會獲取當前 observers 的數(shù)量,如果不存在就是 0,這里的 this 就是 s
對象。
export function createSignal<T>(value?: T, options?: SignalOptions<T>): Signal<T | undefined> { // ... return [readSignal.bind(s), setter]; }
接下來分別判斷 Listener.sources 和 this.observers 是否存在,如果不存在會創(chuàng)建數(shù)組,并建立依賴關系。最后將 s 對象的 value 返回。這里的 value 就是我們調用 createSignal 傳入的初始值。
不同于 vue 中 通過 Proxy 或者 Object.defineProperty 進行屬性劫持,solid.js 中的依賴關系建立是通過函數(shù)調用實現(xiàn)的,例如在 createEffect
中調用 getter
函數(shù),這時就會建立依賴關系。
writeSignal
我們已經知道,通過 getter 可以建立 compulation 和 state 之間的依賴關系。setter 函數(shù)其實就是用來觸發(fā)依賴。
export function writeSignal(node: SignalState<any> | Memo<any>, value: any, isComp?: boolean) { let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value; if (!node.comparator || !node.comparator(current, value)) { if (Transition) { const TransitionRunning = Transition.running; if (TransitionRunning || (!isComp && Transition.sources.has(node))) { Transition.sources.add(node); node.tValue = value; } if (!TransitionRunning) node.value = value; } else node.value = value; if (node.observers && node.observers.length) { runUpdates(() => { for (let i = 0; i < node.observers!.length; i += 1) { const o = node.observers![i]; const TransitionRunning = Transition && Transition.running; if (TransitionRunning && Transition!.disposed.has(o)) continue; if ((TransitionRunning && !o.tState) || (!TransitionRunning && !o.state)) { if (o.pure) Updates!.push(o); else Effects!.push(o); if ((o as Memo<any>).observers) markDownstream(o as Memo<any>); } if (TransitionRunning) o.tState = STALE; else o.state = STALE; } if (Updates!.length > 10e5) { Updates = []; if ("_SOLID_DEV_") throw new Error("Potential Infinite Loop Detected."); throw new Error(); } }, false); } } return value; }
writeSignal 接收兩個參數(shù),第一個參數(shù)就是 state 對象,第二個參數(shù)就是我們傳入的值。
首先獲取 current,我們暫時忽略 Transition 相關的判斷邏輯,這里的 current 就是 state 的值,也就是舊值。
當 node.comparator
為 false,或者新值和舊值不同時,才會進行賦值和觸發(fā)更新。
comparator 可以被賦值為 false 或者一個函數(shù),默認 comparator 只會比較新值和舊值引用是否相同,這里我們后面再去分析。
當傳入的值與舊值不同,將新值賦值 node.value
。然后判斷 node 是否存在觀察者,如果存在會循環(huán)遍歷 observers 數(shù)組,根據(jù)不同邏輯放入 Updates 或者 Effects 數(shù)組,不過最終都會執(zhí)行 observer 對象,即 computation 對象。
案例分析
我們可以通過源碼調試的方式對代碼進行分析。
我們來看下面這個例子。
const { createSignal } = require("../../solid/dist/solid.cjs"); const [count, setCount] = createSignal(0); console.log(count()); setCount(count() + 1); console.log(count());
例子很簡單,就是創(chuàng)建一個響應式數(shù)據(jù),打印它,改變值后繼續(xù)對其進行打印。
當 createSignal 函數(shù)被執(zhí)行完畢之前,我們可以可以看到 s 對象已經被創(chuàng)建,value 值為 0,observers 為 null。
接下來執(zhí)行第一次打印,這時會觸發(fā) readSignal 函數(shù)。
可以看到,this 其實就是 state 對象,此時 runningTransition 和 Listerner 都為空,什么都不會執(zhí)行,直接返回 s.value。
當執(zhí)行到 setCount(count() + 1)
這段代碼時,首先會取到 state 的 value 值,然后再進行計算,并將結果傳給 setter 函數(shù),觸發(fā) writeSignal
函數(shù)。
可以看到,current 的值是 0,此時 comparator 肯定是存在的,并且兩個值并不相等,由于 Transition 不存在,所以會將 value 賦值給 node.value
,此時 state 的 value 值已經變?yōu)?1。由于 node.observers` 也不存在,所以會直接返回傳入的 value ,函數(shù)執(zhí)行完畢。
接下來執(zhí)行最后一次打印,和之前的過程一樣,這里只是做了一次取值操作,打印出改變后的結果 1。
我們還可以調試其他案例,比如給 createSignal
傳遞第二個參數(shù),配置 name 和 equals 屬性然后查看代碼的變化。
總結
createSignal 用于創(chuàng)建響應式數(shù)據(jù),其內部定義了一個 s 對象,保存當前值和依賴關系,并返回 getter 函數(shù)和 setter 函數(shù)。
當調用 getter 函數(shù)讀取值時,如果存在 Listener,雙方會建立依賴關系,即將 Listener 添加到 state 的 observers 數(shù)組中,將 state 添加到 Listener 的 sources 數(shù)組中, 并返回當前值。
當調用 settter 函數(shù)賦值時,如果存在 observers,會遍歷 observers 數(shù)組,并根據(jù)邏輯加入 Updates 或 Effects 數(shù)組中,最后去執(zhí)行它們,觸發(fā)副作用函數(shù)執(zhí)行。
以上就是solid.js響應式createSignal 源碼解析的詳細內容,更多關于solid.js createSignal源碼的資料請關注腳本之家其它相關文章!
相關文章
在vue中通過render函數(shù)給子組件設置ref操作
這篇文章主要介紹了在vue中通過render函數(shù)給子組件設置ref操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11vue 不使用select實現(xiàn)下拉框功能(推薦)
這篇文章主要介紹了vue 不使用select實現(xiàn)下拉框功能,在文章給大家提到了vue select 組件的使用與禁用,需要的朋友可以參考下2018-05-05vue+ElementUi+iframe實現(xiàn)輪播不同的網(wǎng)站
需要實現(xiàn)一個輪播圖,輪播內容是不同的網(wǎng)站,并實現(xiàn)鼠標滑動時停止輪播,當鼠標10秒內不動時繼續(xù)輪播,所以本文給大家介紹了用vue+ElementUi+iframe實現(xiàn)輪播不同的網(wǎng)站,需要的朋友可以參考下2024-02-02vue+element ui el-tooltip動態(tài)顯示隱藏問題
這篇文章主要介紹了vue+element ui el-tooltip動態(tài)顯示隱藏問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10Vue3使用postcss-px-to-viewport實現(xiàn)頁面自適應
postcss-px-to-viewport 是一個 PostCSS 插件,它可以將 px 單位轉換為視口單位,下面我們就看看如何使用postcss-px-to-viewport實現(xiàn)頁面自適應吧2024-01-01解決vue項目Error:Cannot find module‘xxx’類報錯問題
當npm運行報錯Error:Cannot find module 'xxx'時,通常是因為node_modules文件或依賴未正確安裝,解決步驟包括刪除node_modules和package-lock.json文件,重新運行npm install,并根據(jù)需要安裝額外插件,若網(wǎng)絡問題導致安裝失敗2024-10-10