Signals大火!為何眾多前端主流框架都要實現它?

高級前端進階 發佈 2023-06-06T03:43:22.730924+00:00

大家好,很高興又見面了,我是「高級前端‬進階‬」,由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點讚、收藏、轉發!例如:在 a+b=c 的場景,在傳統編程方式下如果 a、b 發生變化,那麼需要重新計算 a+b 來得到 c 值。

家好,很高興又見面了,我是"高級前端‬進階‬",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點讚、收藏、轉發!

今天給大家介紹的主題是反應式編程,開始之前也可以提前閱讀我的另一篇已經發表的討論Signal的文章:

  • 《為什麼說 useSignal() 是 Web 框架的未來!》

話不多說,直接開始!

1.什麼是反應式編程

反應式編程是一種編程思想與方式,是為了簡化並發編程而出現的。與傳統的處理方式相比,反應式編程能夠基於數據流中的事件進行反應處理

例如:在 a+b=c 的場景,在傳統編程方式下如果 a、b 發生變化,那麼需要重新計算 a+b 來得到 c 值。而反應式編程中,不需要重新計算。a、b 的變化事件會觸發 c 的值自動更新。這種方式類似於在消息中間件中常見的發布/訂閱模式。由流發布事件,而代碼邏輯作為訂閱方基於事件進行處理,並且是異步執行的。

反應式編程中,最基本的處理單元是事件流(事件流是不可變的,對流進行操作只會返回新的流)中的事件。核心是基於事件流、無阻塞、異步的,使用反應式編程不需要編寫底層的並發、並行代碼。並且由於其聲明式編寫代碼的方式,使得異步代碼易讀且易維護。

常用的反應式編程類庫包括:Reactor、RxJava 2、Vert.x 以及 Ratpack 等等。

2.可以自動化執行的邏輯都將自動執行

當 React 被引入時,比其他任何庫和框架都更加吸引開發者,它引入了一個非常有趣的概念稱為單向數據綁定,或者更簡單地說,作為虛擬 DOM 的一部分引入的單向數據流。

它提供了一種全新的體驗,即當數據狀態發生變化時,開發人員不必考慮更新如何在 UI 中流動。然而,隨著越來越多的 hooks 被引入,有一些語法規則可以確保它們以最佳方式執行。 從本質上講,與 React 的原始目的有偏差,即單向流或顯式突變(explicit mutations)。比如:

  • 自動正確填寫依賴數組
  • 自動記住正確的值或回調以進行渲染優化
  • 有意識地避免 prop drilling

然而,如果無法有效、正確地避免以上情況,可能導致一些嚴重的性能問題,即一股腦的重新渲染。 這與僅編寫組件來構建 UI 的初衷略有不同。

props drilling 是數據以 props 的形式從 React 組件樹中的一部分傳遞到另一部分, 只是傳遞的組件層級過深、而中間層組件並不需要這些 props,只是做一個向下轉發, 這種情況就叫做 props drilling。

信號(Signals)採用反應式編程原語來幫助消除複雜性,並通過將注意力轉移到正確的事情上來幫助改善開發人員體驗,而不必明確遵循一組語法規則來獲得性能提升。

3.什麼是信號

信號是反應式編程的關鍵原語(Primitive)之一。從語法上講,它們與 React 中的狀態管理非常相似。然而,信號的反應式能力賦予了它的諸多優勢。比如下面的例子:

const [State, setState] = useState(0);
// state -> value
// setState -> setter
const [signal, setSignal] = createsignal(0);
// signal -> getter
// setSignal -> setter

看起來幾乎相同,除了 useState 返回一個值而 useSignal 返回一個 getter 函數。

信號在其概念中相當於一個值的框,當一個框中的值發生變化時,所有相關框中的值都會自動更新。 Signal 會重複該過程,直到更新所有框。

乍一看,這非常類似於 useState 和 useEffect 的組合,但請注意信號是全局定義的。 這允許,當框中的值更改時,僅刷新 VirtualDOM 中依賴於它的那些組件。

import { signal, computed } from "@preact/signals";
const count = signal(0);
// computed
const double = computed(() => count.value * 2);
effect(() => console.log(double.value)));
function Counter() {
  return (
    <button onClick={() => count.value++}>
      {count} x 2 = {double}
    </button>
  );
}

當然,這並不是 Signals 優勢的全部。 在應用程式的整個生命周期中,信號引用不會改變,因此開發者不必擔心多餘的渲染。 computed 和 effect 函數都不需要依賴項列表而是會自動檢測。 此外,如果依賴關係發生變化,但最終值保持不變,也不會刷新依賴關係。

本質上,Signals 是一個用純 JavaScript 編寫的庫, 如果開發者在 JSX 中使用 signal,而不是 signal.value,它將被視為一個單獨的組件。 這意味著如果它的值發生變化,不會渲染整個父組件,只會刷新一小段文本。

// 在此示例中,整個 Counter 組件將在計數更改時重新渲染
const count = signal(0);
function Counter() {
  return (
    <>
      <SomeOtherComponent />
      Value: {count.value}
    </>
  );
}
// 在此示例中,只有計數值會在計數更改時重新渲染
const count = signal(0);
function Counter() {
  return (
    <>
      <SomeOtherComponent />
      Value: {count}
    </>
  );
}

文章前面部分講過,Signals 實際上是觀察者模式的簡單實現,當檢索一個值時同時訂閱了它。 當設置一個值時,實際上是發出一個事件。 真正的魔力始於 Signlas 和框架之間的接口。 為了方便開發人員,創建者應用了一些有爭議的技巧,例如:重寫 React.createElement 函數。

4.信號比狀態好在哪裡

一旦 useState 返回一個值,庫通常不再關心該值的使用。 開發人員必須自己決定在何處使用該值,並且必須確保想要訂閱該值更改的任何 Effects、Memos 或 Callbacks 都在其依賴列表中提到該值。

除此之外,記住該值以避免不必要的重新渲染,這顯然對開發者提出了更高的要求。

  • Signal: 是最簡單的原語(Primitive),它們包含值,以及 get 和 set 函數,因此我們可以在讀取和寫入的時候進行攔截
  • Effects:Effect 是讀取 Signal 的封裝函數,並且會在依賴的 Signal 值發生變化時重新執行。這對於創建諸如渲染之類副作用很有用。
  • Memos:Memo 是緩存的派生值,有著 Signal 和 Effect 相同的屬性。Memo 跟蹤自己的 Signal,僅在這些 Signal 發生變化時重新執行,並且本身是可跟蹤的 Signal

比如下面的ParentComponent組件:

function ParentComponent() {
  const [state, setState] = useState(0);
  const stateVal = useMemo(() => {
    return doSomeExpensiveStateCalculation(state);
  }, [state]);
  // 顯式記憶並確保依賴關係準確

  useEffect(() => {
    sendDataToServer(state);
  }, [state]);
  // 顯式調用狀態訂閱

  return (
    <div>
      <ChildComponent stateVal={stateVal} />
    </div>
  );
}

createSignal 返回一個 getter 函數,因為信號本質上是反應性的。 為了進一步分解,信號跟蹤誰對狀態的變化感興趣,如果發生變化,它會通知這些訂閱者。

為了獲得此訂閱者信息,信號會跟蹤調用這些狀態獲取器(本質上是一個函數)的上下文,調用 getter 創建訂閱。這非常有用,因為庫本身可以自行管理訂閱狀態更改的訂閱者,並在更改後通知他們,而無需開發人員明確調用它。

createEffect(() => {
  updateDataElswhere(state());
});
// effect 僅在 `state` 改變時運行 - 自動訂閱

調用 getter 的上下文(不要與 React Context API 混淆)是庫唯一會通知的上下文,這意味著記憶、顯式填充大型依賴項數組以及修復不必要的重新渲染都可以有效避免。有助於避免使用大量用於此目的的額外 Hooks,例如 useRef、useCallback、useMemo 和大量重新渲染。

Signal 極大地增強了開發人員的體驗,並將重點轉移回為 UI 構建組件,而不是花費額外的 10% 的開發人員精力來遵守嚴格的性能優化語法規則。

function ParentComponent() {
  const [state, setState] = createSignal(0);
  const stateVal = doSomeExpensiveStateCalculation(state());
  // 不需要顯式記憶

  createEffect(() => {
    sendDataToServer(state());
  });
  // 只有在狀態改變時才會被觸發 - 效果會自動添加為訂閱者

  return (
    <div>
      <ChildComponent stateVal={stateVal} />
    </div>
  );
}

5.本文總結

一般而言,使用信號和反應式編程似乎存在非常偏見的立場。 然而,事實並非如此。

React 是一個高性能、優化的庫。儘管在以最佳方式使用狀態方面存在一些差距或遺漏,從而導致不必要的重新渲染,但它仍然非常快。 在以某種方式使用 React 多年之後,前端開發人員已經習慣於將特定的數據流可視化並重新渲染,用反應式編程思維完全取代它在一定程度上顯得跛腳。但是,React 仍然是構建用戶界面的優秀選擇。

反應式編程,除了性能增強之外,還通過歸結為三個主要原語:Signal、Memo 和 Effects,使開發人員的體驗更加簡單。 這有助於開發者更多地關注為 UI 構建組件,而不是擔心顯式處理性能優化。

目前,Signal 也越來越受歡迎,並且是許多現代 Web 框架的一部分,例如 Solid.js、Preact、Qwik 和 Vue.js。

因為篇幅有限,文章並沒有過多展開,如果有興趣,可以在我的主頁繼續閱讀,同時文末的參考資料提供了大量優秀文檔以供學習。最後,歡迎大家點讚、評論、轉發、收藏!

參考資料

https://www.velotio.com/engineering-blog/why-signals-could-be-the-future-for-modern-web-frameworks

https://www.solidjs.com/guides/reactivity#how-it-works

https://www.builder.io/blog/usesignal-is-the-future-of-web-frameworks#what-signal-is

https://vived.io/signal-a-new-way-to-manage-application-state-frontend-weekly-vol-104/

https://cloud.tencent.com/developer/article/1602301

https://blog.csdn.net/wumu0927/article/details/122288050

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

https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob

關鍵字: