SolidJS響應式原理和簡易實現
上篇文章中主要介紹了Solid JS的基本語法,分階段粗略地介紹了一些原理(響應式原理、編譯原理和運行時原理)。
接下來的幾篇文章里我會詳細介紹每個階段的詳細實現原理,希望可以給你的學習帶來幫助。
寫這篇文章的時候有很大的猶豫,擔心Solid JS受眾太小,文章的反響連”平平“都算不上,所以先寫一篇試試水,如果真的反響平平,我會暫時放棄這個寫作計劃,還請見諒!
響應式原理
作為Solid JS響應式的基石,我們先看看createSignal的用法和原理。接著我們手動實現一個簡易版的createSignal,
?? 萬惡之源createSignal
?? 用法
function createSignal<T>(
initialValue: T,
options?: { equals?: false | ((prev: T, next: T) => boolean) }
): [get: () => T, set: (v: T) => T];
Solid JS的厲害之處是,你可以定義變量是否為響應式,甚至可以定義響應式的時機。
- ?? 僅提供initialValue時,(默認)是響應式的。
- ?? 在options設置equals為false時不管何時都是響應式。
- ?? equals設置為函數,根據新值和舊值的關系來設置何時為響應式。
?? 例子
下面這個例子僅僅在新的值大于舊的值(新增)時,才是響應式的。
import { render } from "solid-js/web";
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1, { equals: (n, o) => n > o });
const increment = () => setCount(count() + 1);
const reduce = () => setCount(count() - 1);
return (
<>
<button type="button" onClick={increment}>
+
</button>
<button type="button">{count()}</button>
<button type="button" onClick={reduce}>
-
</button>
</>
);
}
render(() => <Counter />, document.getElementById("app")!);
?? 原理
createSignal簡化后的邏輯如下:
?? 實現
const signalOptions = {
equals: false
};
function createSignal(value, options) {
// 初始化options
options = options
? Object.assign({}, signalOptions, options)
: signalOptions;
// 創建內部signal
const s = {
value,
comparator: options.equals || undefined
};
// 定義setter
const setter = value => {
if (typeof value === "function") {
value = value(s.value);
}
return writeSignal(s, value);
};
// 返回[getter, setter]
return [readSignal.bind(s), setter];
}
// 返回當前內部signal的value
function readSignal() {
return this.value;
}
// 更新內部的value,然后返回value
function writeSignal(node, value) {
if (!node.comparator) {
node.value = value;
}
return value;
}
現在我們已經實現了createSignal基本功能了,接下來我們通過實現createEffect來讓它具有響應式的能力。
??createEffect
?? 用法
createEffect接受一個副作用函數,每當它依賴的狀態發生改變時,這個副作用都被執行一次。
function createEffect<T>(fn: (v: T) => T, value?: T): void;
?? 例子
這是個很常見的例子。
import { render } from "solid-js/web";
import { createSignal, createEffect } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
createEffect(() => console.log('count : ', count()))
return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}
render(() => <Counter />, document.getElementById("app")!);
?? 原理
我們已經知道,當createEffect依賴項發生改變時,副作用會也會發生改變,這是因為createSignal是基于發布訂閱模式的響應式。一個較為完整的關系如下:
?? 實現
const signalOptions = {
equals: false
};
const observers = []
function createEffect (effect) {
const execute = () => {
// 保存在observers中
observers.push(execute);
try {
effect();
} finally {
// 釋放
observers.pop();
}
};
// 副作用函數立即執行
execute();
};
function createSignal(value, options) {
// 初始化options
options = options
? Object.assign({}, signalOptions, options)
: signalOptions;
// 創建內部signal
const s = {
value,
// 保存訂閱者
subscribers: new Set(),
comparator: options.equals || undefined
};
// 定義setter
const setter = value => {
if (typeof value === "function") {
value = value(s.value);
}
return writeSignal(s, value);
};
// 返回[getter, setter]
return [readSignal.bind(s), setter];
}
// 返回當前內部signal的value
function readSignal() {
const curr = observers[observers.length - 1]
curr && this.subscribers.add(curr)
return this.value;
}
// 更新內部的value,然后返回value
function writeSignal(node, value) {
if (!node.comparator) {
node.value = value;
}
// 每次寫入時執行對應的訂閱者
node.subscribers.forEach((subscriber) => subscriber());
return value;
}
現在我們準備下面的html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SolidJS</title>
</head>
<body>
<h1>打開控制臺查看結果</h1>
<script src="./solid.js"></script>
<script>
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
createEffect(() => console.log('count : ', count()))
window.increment = increment
</script>
</body>
</html>
使用window.increment模擬點擊事件,打印如下。
下面我們實現createMemo
?? createMemo
?? 用法
createMemo通常用來做派生變量保存基于某個狀態中間值。完整用法如下:
function createMemo<T>(
fn: (v: T) => T,
value?: T,
options?: { equals?: false | ((prev: T, next: T) => boolean) }
): () => T;
本篇只討論最原始的memo。
?? 例子
一個例子如下,每當count變化時,sum自動加2
import { render } from "solid-js/web";
import { createSignal, createEffect, createMemo } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
const sum = createMemo(() => count() + 2)
createEffect(() => console.log('sum : ', sum()))
createEffect(() => console.log('count : ', count()))
return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}
render(() => <Counter />, document.getElementById("app")!);
?? 原理
它的內部是使用createSignal實現的,所以流程上來說和createEffect一樣。
真實的源碼里,是基于createComputation實現的,但是它的內部是createSignal
?? 實現
const createMemo = (memo) => {
const [value, setValue] = createSignal();
createEffect(() => setValue(memo()));
return value;
};
接下來在測試例子里添加如下兩行
const sum = createMemo(() => count() + 2)
createEffect(() => console.log('sum : ', sum()))
然后在控制臺操作