從 Signals 看響應式狀態管理
🙋🏻♀️ 編者按:本文作者是螞蟻集團前端工程師新羅,介紹了響應式狀態管理的一些理念和方法,希望能夠給大家帶來一些參考。
什麼是 Signals?
我們先從 Preact 作者 Jason Miller 發佈的一篇文章說起
Preact 引入了 Signals,提供了快速的響應式狀態原語(或者叫原子吧),那麼 Signals 有以下幾點:
-
感覺上像是使用原始數據結構
-
能根據值的變化自動更新
-
直接更新 DOM (換句話來說無 VDOM)
-
沒有依賴數組
貼一下 Signals 的用法:
import { signal } from "@preact/signals";
const count = signal(0);
function Counter() {
const value = count.value;
const increment = () => {
count.value++;
}
return (
<div>
<p>Count: {value}</p>
<button onClick={increment}>click me</button>
</div>
);
}
可以看到,跟 SolidJS 的 createSignal
非常相似,而且兩者有很多共同點(下面再說),另外通過.value
訪問屬性非常類似於 Vue 中的 Ref。Signals 可以在一個應用從小到大,在越來越複雜的邏輯迭代後,依然能保證性能。Singals 提供了細粒度狀態管理的好處,而無需通過 memorize 或者其他 tricks 方式去優化,Signals 跳過了數據在組件樹中的傳遞,而是直接更新所引用的組件。這樣開發者就能降低使用心智,保證性能最佳。
下面是 hooks state 作爲原子和 Signals 的性能比對:
能達到如此表現,Signals 有以下幾點:
-
默認惰性求值(lazy evaluate)- 只有被使用到的纔會被監聽和更新
-
最佳更新策略
-
最佳依賴追蹤策略 - 不像 hooks 需要指定依賴
-
直接訪問狀態值,不需要 selector 或其他 hooks
另外從上面可以看出 Signals 是可以獨立於組件外的,跟 hooks 方式不一樣
那麼是不是 Signals 既可以服務於 Preact,也能集合到其他框架中呢?從 Signals 目標中,我們看到了這點:
而且目前,Signals 可以單獨品嚐使用,不用依附 UI 框架。比如:https://codesandbox.io/s/tender-burnell-7c7g9n?file=/src/index.js
其他框架
我們把目光切換到 SolidJS ,先看一下 SolidJS 響應式數據例子:
import { createSignal, onCleanup } from "solid-js";
import { render } from "solid-js/web";
const App = () => {
const [count, setCount] = createSignal(0);
const timer = setInterval(() => setCount(count() + 1), 1000);
onCleanup(() => clearInterval(timer));
return <div>{count()}</div>;
};
render(() => <App />, document.getElementById("app"));
可以看到 SolidJS 響應式也是也 Signal 作爲基礎,createSignal
既可以用於組件內,也可以用於組件外,這個跟 Preact 中類似。一方面可以將 Signal 作爲組件的 local state,也可以定義爲 global State。與前面類似,SolidJS 中也有以下相似點:
-
響應式細粒度更新
-
無需定義 dependencies
-
惰性取值
SolidJS 與 Mobx 和 Vue 的響應式非常相似,但是不會處理 VDOM,而是直接更新 DOM。所以 SolidJS 的性能表現也比較不錯:
這裏不再介紹 Vue 的響應式,有興趣的可以再去了解下
響應式狀態管理三要素
信號: Signals
這裏其實很難翻譯,一個基礎響應式數據怎麼命名,這裏用 Signals 先代替,當然我們有可能會看到比較叫:Observables(Mobx 等),Atoms(Recoil,Jotai 等),Refs(Vue 等)。不過基本意思都一樣,表示一個響應式數據的單元。
const [count, setCount] = createSignal(0);
console.log(count()); //0
setCount(5);
console.log(count()); //5
這裏取值用的是 function,有些地方用的是 .value
,意味着也可以通過 Object 的 getter, setter 或者 Proxy 去進行數據處理
反應: Reactions
反應這裏很好理解,大部分地方叫 Effect ,也就是副作用,當然也有用 actions 的,下方是一個基本例子:
const [count, setCount] = createSignal(0);
createEffect(() => console.log("The count is", count()));
setCount(5);
反應也就是在數據更新時的監聽器,作爲響應式數據的基礎,也是必不可少的一環
衍生: Derivations
這裏是指數據的衍生狀態,本質上也可以認爲是 Signals 的變種,常見命令可能有 computed, memo 等。
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const fullName = createMemo(() => {
return `${firstName()} ${lastName()}`
});
console.log(fullName);
衍生能緩存計算結果,避免重複的計算,並且也能自動追蹤依賴以及同步更新。
響應式特點
響應式數據管理會存儲不同節點之間的鏈接關係,當每次節點更新之後,會重新檢查鏈接關係。如果不在關聯,就會解綁鏈接,取消依賴。
下方的例子更能體現:
const [firstName, setFirstName] = createSignal("a");
const [lastName, setLastName] = createSignal("b");
const [showFullName, setShowFullName] = createSignal(true);
const displayName = createMemo(() => {
if (!showFullName()) return firstName();
return `${firstName()} ${lastName()}`
});
createEffect(() => console.log("名稱:", displayName()));
// a b
setShowFullName(false);
// a
setLastName("c");
// nothging change
setShowFullName(true);
// a c
運行效果可見:https://codesandbox.io/s/tender-burnell-7c7g9n?file=/src/index.js
另外響應式還有一點就是同步更新,同步更新避免了狀態不一致的問題(相信使用 React 的同學深有感受),也提高了更好的預測性和可測試性。在響應式數據更新的基礎上,有些也會加入比如批量更新,批量更新在避免重複執行反應和衍生上大有好處,大大避免了一些多餘額外的執行消耗
手動實現一個
響應式狀態管理核心還是用的觀察者模式,當 Signals 更新時,Reactions 會訂閱到數據變化從而更新數據。
Signals
首先實現一個基礎的數據更新與讀取
const createSignal = (value) => {
const setter = (newValue) => value = newValue;
return [() => value, setter]
}
const [name, setName] = createSignal('a')
console.log(name());
setName('b')
console.log(name());
輸出結果:
加上訂閱邏輯,重新更改下:
// context 包含Reactions中的執行方法和Signal依賴
const context = [];
const createSignal = (value) => {
const subscriptions = new Set();
const readFn = () => {
const running = context.pop();
if (running) {
subscriptions.add({
execute: running.execute
});
running.deps.add(subscriptions);
}
return value;
};
const writeFn = (newValue) => {
value = newValue;
for (const sub of [...subscriptions]) {
sub.execute();
}
};
return [readFn, writeFn];
};
const [name, setName] = createSignal("a");
console.log(name());
setName("b");
console.log(name());
從上述可以看到,在讀取的時候會獲取當前執行的上下文,拿到 Reactions 的方法,並且方法依賴裏增加當前 Signals,這樣 Reactions 就能訂閱到這個 Signals,當 Signals 更新時,會執行所包含的訂閱方法。接下來我們把 Reactions 補充下
Reactions
廢話不多說,直接看代碼
const createEffect = (fn) => {
const execute = () => {
context.push(running);
fn();
context.pop(running);
}
const running = {
execute,
deps: new Set()
}
execute();
}
const [name, setName] = createSignal("a");
createEffect(() => console.log(name()))
setName("b");
但是這裏有個問題就是,隨着 Reactions 每次執行,running 的 deps 會逐步累加,所以需要在執行前,清空 deps。
const createEffect = (fn) => {
const execute = () => {
running.deps.clear();
context.push(running);
try {
fn();
} finally {
context.pop(running);
}
};
const running = {
execute,
deps: new Set()
};
execute();
};
這樣 Reactions 也就 OK 了
Derivations
那麼 Derivations 的代碼就簡單多了
const createMemo = (fn) => {
const [memo, setMemo] = createSignal();
createEffect(() => setMemo(fn()));
return memo;
}
其實也很好理解,如前面所說,衍生是一種特殊的 Signals,所以直接返回 Signal,另外 Reactions 是可以追蹤訂閱到 Signals 的變化,所以在 Reactions 函數里設置 Derivations 的值就可以了。
完整 Demo 見:https://codesandbox.io/s/elastic-blackwell-br79m2?file=/src/index.js
通過短短不到 100 行的代碼就能實現一個基礎的細粒度更新的狀態管理(當然我們這裏用的是方法去取值,也可以用 proxy 等方式,例如 valtio 等),但是僅僅這些到了實際應用和跟 UI 框架融合還是不夠的,需要有更多的完善和補充。如何將響應式代碼融入到渲染過程,可以參考這篇文章(https://indepth.dev/posts/1289/solidjs-reactivity-to-rendering)。也可以看看 Signals 是如何從實際代碼上融入到 Preact 與 React 中的(https://github.com/preactjs/signals/tree/main/packages)。
回到 React
讓我們把目光回到 React 上,從目前來看,React 的狀態管理有很多,可以見雲謙老師的這個文章,不同的框架對比如下:
這樣看來 valtio 更符合 Signals 在 React 中的實現
-
無需 dependency
-
外部 store,無需關聯組件
但是作爲 React 相關的框架,最後都會去做 VDOM 更新,跟 SolidJS 直接更新不一樣,當然我們也期望 React 官方能做一些改變去優化現有的開發體驗,比如:React Forget/ useEvent
最後
這篇文章並不是想指導如何做狀態管理的選型,也不是去分析不同狀態管理的優劣。只是介紹響應式狀態管理的一些理念和方法,希望能對大家有一些價值參考。
引用
-
https://preactjs.com/blog/introducing-signals
-
https://preactjs.com/guide/v10/signals/
-
https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf
-
https://indepth.dev/posts/1289/solidjs-reactivity-to-rendering
-
https://cn.vuejs.org/guide/extras/reactivity-in-depth.html
-
https://my5353.com/gCocL
-
https://mp.weixin.qq.com/s/26_yYH5fbDyMTEKOMcNxtA
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Tn0rbkCdFw4f-3ihKUEYQA