從 Signals 看響應式狀態管理

🙋🏻‍♀️ 編者按:本文作者是螞蟻集團前端工程師新羅,介紹了響應式狀態管理的一些理念和方法,希望能夠給大家帶來一些參考。

 什麼是 Signals?

我們先從 Preact 作者 Jason Miller 發佈的一篇文章說起

Preact 引入了 Signals,提供了快速的響應式狀態原語(或者叫原子吧),那麼 Signals 有以下幾點:

貼一下 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>
  );
}

可以看到,跟 SolidJScreateSignal非常相似,而且兩者有很多共同點(下面再說),另外通過.value訪問屬性非常類似於 Vue 中的 Ref。Signals 可以在一個應用從小到大,在越來越複雜的邏輯迭代後,依然能保證性能。Singals 提供了細粒度狀態管理的好處,而無需通過 memorize 或者其他 tricks 方式去優化,Signals 跳過了數據在組件樹中的傳遞,而是直接更新所引用的組件。這樣開發者就能降低使用心智,保證性能最佳。

下面是 hooks state 作爲原子和 Signals 的性能比對:

能達到如此表現,Signals 有以下幾點:

另外從上面可以看出 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 中也有以下相似點:

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 中的實現

但是作爲 React 相關的框架,最後都會去做 VDOM 更新,跟 SolidJS 直接更新不一樣,當然我們也期望 React 官方能做一些改變去優化現有的開發體驗,比如:React Forget/ useEvent

 最後

這篇文章並不是想指導如何做狀態管理的選型,也不是去分析不同狀態管理的優劣。只是介紹響應式狀態管理的一些理念和方法,希望能對大家有一些價值參考。

 引用

  1. https://preactjs.com/blog/introducing-signals

  2. https://preactjs.com/guide/v10/signals/

  3. https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf

  4. https://indepth.dev/posts/1289/solidjs-reactivity-to-rendering

  5. https://cn.vuejs.org/guide/extras/reactivity-in-depth.html

  6. https://my5353.com/gCocL

  7. https://mp.weixin.qq.com/s/26_yYH5fbDyMTEKOMcNxtA

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Tn0rbkCdFw4f-3ihKUEYQA