說明
支持我們
,由
Marvin HagemeisterJason Miller 撰寫

介紹 Signals

Signals 是一種表達狀態的方式,可確保應用程式保持快速,無論它們變得多麼複雜。Signals 基於反應式原則,並提供極佳的開發人員人體工程學,採用針對虛擬 DOM 最佳化的獨特實作。

在核心部分,signal 是具有 .value 屬性的物件,其中包含一些值。從元件內部存取 signal 的值屬性會在該 signal 的值變更時自動更新該元件。

除了簡單易寫之外,這還能確保狀態更新保持快速,無論您的應用程式有多少元件。Signals 預設為快速,會自動在幕後最佳化更新。

import { signal, computed } from "@preact/signals";

const count = signal(0);
const double = computed(() => count.value * 2);

function Counter() {
  return (
    <button onClick={() => count.value++}>
      {count} x 2 = {double}
    </button>
  );
}
在 REPL 中執行

與 hooks 不同,Signals 可以用在元件內部或外部。Signals 也與 hooks 類別元件一起運作良好,因此您可以按照自己的步調引入它們,並運用您現有的知識。在幾個元件中嘗試它們,並隨著時間逐漸採用它們。

喔,對了,我們始終堅持為您提供最小函式庫的初衷。在 Preact 中使用 Signals 只會為您的套件大小增加 1.6kB

如果您想直接開始,請前往我們的 文件,深入瞭解 Signals。

Signals 解決了哪些問題?

在過去幾年,我們處理過各種應用程式和團隊,從小型新創公司到同時有數百位開發人員提交的巨型公司。在這段期間,核心團隊的每個人都注意到應用程式狀態管理方式的重複問題。

已經產生了許多解決這些問題的絕佳方案,但即使是最好的方案,仍然需要手動整合到架構中。結果,我們看到開發人員猶豫是否採用這些方案,反而偏好使用架構提供的狀態原語來建置。

我們建置 Signals,使其成為一個引人注目的方案,結合最佳效能和開發人員人體工學,並與架構無縫整合。

全球狀態爭議

應用程式狀態通常從小而簡單開始,可能是幾個簡單的 useState 鉤子。隨著應用程式成長,更多元件需要存取同一段狀態,這段狀態最終會提升到共同祖先元件。此模式會重複多次,直到大部分狀態最後都存活在元件樹的根部附近。

Image showing how the depth of the component tree directly affects rendering performance when using standard state updates.

此情境對傳統的基於虛擬 DOM 的架構構成挑戰,這些架構必須更新受狀態無效化影響的整個樹狀結構。基本上,渲染效能是該樹狀結構中元件數量的函數。我們可以使用 memouseMemo 記住元件樹狀結構的部分,以便架構接收相同的物件。當沒有任何變更時,這會讓架構略過渲染樹狀結構的部分。

雖然這在理論上聽起來很合理,但實際情況通常混亂許多。實際上,隨著程式碼庫的成長,很難確定應該在哪裡放置這些最佳化。通常,即使是善意的記憶化也會因不穩定的相依性值而變得無效。由於鉤子沒有可以分析的明確相依性樹狀結構,因此工具無法協助開發人員診斷相依性不穩定的原因

Context chaos

團隊用來達成狀態共用的另一個常見解決方法是將狀態放入 context 中。這允許透過潛在跳過 context 提供者與使用者之間的元件渲染,來短路渲染。但有一個陷阱:只有傳遞給 context 提供者的值可以更新,而且只能整體更新。更新透過 context 公開的物件上的屬性並不會更新該 context 的使用者,無法進行細微更新。處理此問題的可用選項是將狀態拆分為多個 context,或是在任何屬性變更時透過複製它來使 context 物件失效。

Context can skip updating components until you read the value out of it. Then it's back to memoization.

一開始將值移到 context 中似乎是值得的折衷,但為了共用值而增加元件樹大小的缺點最終會成為一個問題。業務邏輯不可避免地會依賴多個 context 值,這可能會迫使它在樹中的特定位置實作。在樹的中央新增訂閱 context 的元件代價很高,因為它會減少更新 context 時可以跳過的元件數量。更重要的是,訂閱者底下的任何元件現在都必須重新渲染。解決此問題的唯一方法是大量使用記憶化,這讓我們回到記憶化固有的問題。

尋找管理狀態的更好方法

我們回到繪圖板,尋找下一代狀態基元。我們希望創造一個同時解決目前解決方案中問題的東西。手動架構整合、過度依賴記憶化、次佳的 context 使用,以及缺乏程式化可觀察性感覺很落後。

開發人員需要透過這些策略「選擇」效能。如果我們可以反轉它,並提供一個預設快速的系統,讓最佳效能變成你必須努力選擇不用的東西,會怎樣?

我們對這些問題的答案是 Signals。這是一個預設為快速的系統,無需在整個應用程式中使用記憶化或技巧。無論狀態是全域性的、透過 props 或 context 傳遞的,或是元件本地的,Signals 都能提供細緻狀態更新的好處。

Signals to the future

Signals 背後的主要概念是,我們不直接透過元件樹傳遞值,而是傳遞包含值的 Signals 物件(類似於 ref)。當 Signals 的值變更時,Signals 本身保持不變。因此,Signals 可以更新,而不會重新渲染已傳遞的元件,因為元件看到的是 Signals,而不是它的值。這讓我們可以跳過所有渲染元件的昂貴工作,並立即跳轉到實際存取 Signals 值的樹狀結構中特定元件。

Signals can continue to skip Virtual DOM diffing, regardless of where in the tree they are accessed.

我們利用了應用程式的狀態圖通常遠比其元件樹淺的事實。這會導致更快的渲染,因為與元件樹相比,更新狀態圖所需的工作少得多。在瀏覽器中測量時,這種差異最為明顯 - 下面的螢幕截圖顯示了同一應用程式的 DevTools Profiler 軌跡,測量了兩次:一次使用 hooks 作為狀態原語,另一次使用 Signals

Showing a comparison of profiling Virtual DOM updates vs updates through signals which bypasses nearly all of the Virtual DOM diffing.

Signals 版本大幅優於任何傳統 Virtual DOM 基礎架構的更新機制。在我們測試過的一些應用程式中,Signals 快到很難在火焰圖中找到它們。

Signals 翻轉了效能的定位:Signals 預設為快速,而不是透過記憶化或選擇器選擇加入效能。使用 Signals 時,效能是可以選擇退出的(透過不使用 Signals)。

為了達到這種效能水準,Signals 建立在這些關鍵原則上

  • 預設為 Lazy:只有目前在某處使用的 Signals 會被觀察和更新 - 未連接的 Signals 不會影響效能。
  • 最佳更新:如果 Signals 的值沒有變更,使用該 Signals 值的元件和效果就不會更新,即使 Signals 的依賴項已變更。
  • 最佳依賴項追蹤:這個架構會追蹤所有依賴項的 Signals - 不像 hooks 那樣有依賴項陣列。
  • 直接存取:在元件中存取訊號的值會自動訂閱更新,不需要選擇器或掛勾。

這些原則讓訊號非常適合廣泛的用例,甚至與呈現使用者介面無關的場景。

將訊號帶入 Preact

在找出正確的狀態基本元素後,我們開始將它連接到 Preact。我們一直喜愛掛勾的原因是它們可以直接在元件內部使用。與通常依賴「選擇器」函式或將元件包裝在特殊函式中以訂閱狀態更新的第三方狀態管理解決方案相比,這是一種人體工學優勢。

// Selector based subscription :(
function Counter() {
  const value = useSelector(state => state.count);
  // ...
}

// Wrapper function based subscription :(
const counterState = new Counter();

const Counter = observe(props => {
  const value = counterState.count;
  // ...
});

這兩種方法對我們來說都不令人滿意。選擇器方法需要將所有狀態存取包裝在選擇器中,這對於複雜或巢狀狀態來說會變得繁瑣。將元件包裝在函式中的方法需要手動將元件包裝起來,這會帶來許多問題,例如遺失元件名稱和靜態屬性。

在過去幾年中,我們有機會與許多開發人員密切合作。一個常見的困難,特別是對於 (p)react 新手來說,是選擇器和包裝器等概念是額外的範例,必須在對每個狀態管理解決方案感到滿意之前學習。

理想情況下,我們不需要了解選擇器或包裝器函式,並且可以在元件中直接存取狀態

// Imagine this is some global state and the whole app needs access to:
let count = 0;

function Counter() {
 return (
   <button onClick={() => count++}>
     value: {count}
   </button>
 );
}

程式碼很清楚,很容易理解正在發生什麼事,但不幸的是它無法運作。元件在按鈕按一下時不會更新,因為沒有辦法知道 count 已變更。

不過,我們無法將這個場景從腦海中抹去。我們可以做些什麼,才能將如此清晰的模型變成現實?我們開始使用 Preact 的可插入式渲染器,對各種想法和實作進行原型製作。這花了一些時間,但我們最終找到了一個實現它的方法

// Imagine this is some global state that the whole app needs access to:
const count = signal(0);

function Counter() {
 return (
   <button onClick={() => count.value++}>
     Value: {count.value}
   </button>
 );
}
在 REPL 中執行

沒有選取器、沒有包裝函式,什麼都沒有。存取訊號值就足以讓元件知道,當該訊號值變更時,它需要更新。在幾個應用程式中測試完原型後,很明顯我們已經掌握了一些訣竅。用這種方式撰寫程式碼感覺很直覺,而且不需要任何心智體操,就能讓所有事情保持最佳運作狀態。

我們可以跑得更快嗎?

我們本可以就此打住,並按原樣發布訊號,但這是 Preact 團隊:我們需要看看我們可以將 Preact 整合推到多遠。在上面的 Counter 範例中,count 的值僅用於顯示文字,這真的不應該需要重新渲染整個元件。與其在訊號值變更時自動重新渲染元件,如果我們只重新渲染文字會如何?更好的是,如果我們完全繞過虛擬 DOM,並直接在 DOM 中更新文字會如何?

const count = signal(0);

// Instead of this:
<p>Value: {count.value}</p>

// … we can pass the signal directly into JSX:
<p>Value: {count}</p>

// … or even passing them as DOM properties:
<input value={count} onInput={...} />

所以是的,我們也這樣做了。你可以將訊號直接傳遞到 JSX 中,在任何你通常會使用字串的地方。訊號值將會被渲染為文字,並且會在訊號變更時自動更新。這也適用於道具。

下一步

如果您感到好奇並想直接深入了解,請前往我們的 文件 以取得信號。我們很樂意聽取您如何使用它們。

請記住,沒有必要急於切換到信號。Hook 將繼續受到支援,它們與信號搭配使用時也能發揮良好的作用!我們建議逐漸嘗試信號,從幾個組件開始,以習慣這些概念。