反應模型解釋
自從我開始開發應用程式和網站以來已經過去了 10 年,但 JavaScript 生態系統從未像今天這樣令人興奮!
2022 年,社群被「訊號」的概念所吸引,以至於大多數 JavaScript 框架都將它們整合到自己的引擎中。我正在考慮 Preact,它自 2022 年 9 月以來提供了與組件生命週期分離的反應變量;或者最近的 Angular,它於 2023 年 5 月實驗性地實現了 Signals,然後從版本 18 正式開始。其他 JavaScript 函式庫也選擇重新考慮他們的方法...
從 2023 年到現在,我一直在各個專案中使用 Signals。它們的實施和使用簡單性完全說服了我,以至於我在技術研討會、培訓課程和會議期間與我的專業網絡分享了它們的好處。
但最近,我開始問自己這個概念是否真正具有「革命性」/是否有訊號的替代品?因此,我更深入地研究了這種反思,並發現了反應式系統的不同方法。
這篇文章概述了不同的反應模型,以及我對它們如何運作的理解。
注意: 說到這裡,你可能已經猜到了,我不會討論Java 的「Reactive Streams」;否則,我會把這篇文章的標題定為「背壓是什麼鬼! 理論
”,但特別是“反應性”。
響應式程式設計是一種開發範例,允許將資料來源的變更自動傳播給消費者。 因此,我們可以將
反應性定義為根據資料的變化即時更新依賴關係的能力。
NB:簡而言之,當使用者填寫和/或提交表單時,我們必須對這些變更做出反應,顯示載入元件或任何其他指定正在發生的事情。 .. 另一個例子,當非同步接收資料時,我們必須透過顯示全部或部分資料、執行新操作等來做出反應 在這種情況下,反應式函式庫提供了自動更新和高效傳播的變量,使編寫簡單且最佳化的程式碼變得更加容易。
為了提高效率,當且僅當它們的值發生變化時,這些系統必須重新計算/重新評估這些變數!同樣,為了確保廣播的數據保持一致和最新,系統必須避免顯示任何中間狀態(特別是在狀態變化的計算期間)。
NB:狀態是指程式/應用程式整個生命週期中使用的資料/值。
好吧,但是…這些「反應模型」到底是什麼?
第一個反應模型稱為「PUSH」(或「渴望」反應)。本系統基於以下原則:
如您可能已經猜到的,「PUSH」模型依賴「Observable/Observer」設計模式。
讓我們考慮以下初始狀態,
let a = { firstName: "John", lastName: "Doe" }; const b = a.firstName; const c = a.lastName; const d = `${b} ${c}`;
使用反應式函式庫(例如 RxJS),這個初始狀態看起來比較像這樣:
let a = observable.of({ firstName: "John", lastName: "Doe" }); const b = a.pipe(map((a) => a.firstName)); const c = a.pipe(map((a) => a.lastName)); const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));
注意:為了這篇文章的目的,所有程式碼片段都應被視為「偽代碼」。
現在,我們假設消費者(例如元件)希望在資料來源更新時記錄狀態 D 的值,
d.subscribe((value) => console.log(value));
我們的元件將訂閱資料流;它仍然需要觸發改變,
a.next({ firstName: "Jane", lastName: "Doe" });
從那裡,「PUSH」系統偵測到變更並自動將其廣播給消費者。基於上面的初始狀態,以下是可能發生的操作的描述:
該系統的挑戰之一在於計算順序。事實上,根據我們的用例,您會注意到 D 可能會被評估兩次:第一次使用 C 的先前狀態值;第二次使用 C 的值。第二次,C 的值是最新的!在這個反應模型中,這個挑戰被稱為「鑽石問題」 ️。
現在,我們假設該狀態依賴兩個主要資料來源,
let a = { firstName: "John", lastName: "Doe" }; const b = a.firstName; const c = a.lastName; const d = `${b} ${c}`;
更新 E 時,系統將重新計算整個狀態,這使得系統可以透過覆蓋先前的狀態來保留單一事實來源。
「鑽石問題」再次發生...這次是在資料來源 C 上,可能會評估 2 次,並且始終在 D 上。
「鑽石問題」並不是「渴望」反應模型中的新挑戰。一些計算演算法(尤其是 MobX 使用的演算法)可以標記「反應式依賴樹的節點」以平衡狀態計算。透過這種方法,系統將首先評估「根」資料來源(在我們的範例中為 A 和 E),然後是 B 和 C,最後是 D。更改狀態計算的順序有助於解決此類問題。
第二個反應模型稱為「PULL」。與“PUSH”模型不同,它基於以下原則:
最重要的是要記住最後一條規則:與先前的系統不同,最後一條規則推遲了狀態計算,以避免對相同資料來源進行多次評估。
讓我們保持之前的初始狀態...
在這種系統中,初始狀態語法將採用以下形式:
let a = observable.of({ firstName: "John", lastName: "Doe" }); const b = a.pipe(map((a) => a.firstName)); const c = a.pipe(map((a) => a.lastName)); const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));
注意: React 愛好者可能會認識這個文法 ?
宣告一個反應變數給一個元組「誕生」:一方面是不可變的變數;另一個變數的更新函數。其餘語句(在我們的例子中為 B、C 和 D)被視為派生狀態,因為它們「監聽」各自的依賴關係。
d.subscribe((value) => console.log(value));
「惰性」系統的定義特徵是它不會立即傳播更改,而是僅在明確請求時傳播更改。
let a = { firstName: "John", lastName: "Doe" }; const b = a.firstName; const c = a.lastName; const d = `${b} ${c}`;
在「PULL」模型中,使用effect()(來自組件)來記錄反應變數(指定為依賴項)的值會觸發狀態變更的計算:
在查詢依賴項時可以最佳化此系統。事實上,在上面的場景中,A 被查詢了兩次以確定它是否已更新。但是,第一個查詢可能足以定義狀態是否已變更。 C 不需要執行此操作...相反,A 只能廣播其值。
讓我們透過加入第二個反應變數「root」來讓狀態稍微複雜一些,
let a = observable.of({ firstName: "John", lastName: "Doe" }); const b = a.pipe(map((a) => a.firstName)); const c = a.pipe(map((a) => a.lastName)); const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));
系統再次延遲狀態計算,直到明確要求為止。使用與之前相同的效果,更新新的反應變數將觸發以下步驟:
由於 A 的值沒有改變,所以不需要重新計算這個變數(同樣的情況也適用於 B 的值)。在這種情況下,使用記憶演算法可以提高狀態計算期間的表現。
最後一個反應模型是「推拉」系統。術語「PUSH」反映了更改通知的立即傳播,而「PULL」指的是按需獲取狀態值。這種方法與所謂的「細粒度」反應性密切相關,它遵循以下原則:
請注意,這種反應性並非「推拉」模型所獨有。細粒度反應性是指對系統依賴性的精確追蹤。因此,還有 PUSH 和 PULL 反應模型也以這種方式工作(我正在考慮 Jotai 或 Recoil。
仍基於先前的初始狀態...「細粒度」反應系統中初始狀態的聲明如下所示:
let a = { firstName: "John", lastName: "Doe" }; const b = a.firstName; const c = a.lastName; const d = `${b} ${c}`;
注意:signal關鍵字的使用不只是軼事?
在語法方面,它與「PUSH」模型非常相似,但有一個顯著且重要的區別:依賴關係! 在「細粒度」反應系統中,沒有必要明確聲明計算派生狀態所需的依賴關係,因為這些狀態隱式追蹤它們使用的變數。在我們的例子中,B 和 C 將自動追蹤 A 值的更改,D 將追蹤 B 和 C 的更改。
let a = observable.of({ firstName: "John", lastName: "Doe" }); const b = a.pipe(map((a) => a.firstName)); const c = a.pipe(map((a) => a.lastName)); const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));
在這樣的系統中,更新反應變數比基本的「PUSH」模型更有效,因為變更會自動傳播到依賴它的衍生變數(僅作為通知,而不是值本身) 。
d.subscribe((value) => console.log(value));
然後,根據需要(讓我們以logger 為例),系統內使用D 將取得關聯根狀態的值(在我們的例子中為A),計算值導出狀態( B和C),最後評估D。這不是一個直覺的操作方式嗎?
讓我們考慮以下狀態,
a.next({ firstName: "Jane", lastName: "Doe" });
再一次,推拉系統的「細粒度」方面允許自動追蹤每個狀態。因此,派生狀態 C 現在追蹤根狀態 A 和 E。更新變數 E 將觸發以下操作:
這是反應性依賴關係相互之間的先前關聯,使得模型如此有效率!
確實,在經典的「PULL」系統(例如React 的Virtual DOM)中,當從元件更新響應式狀態時,框架將收到變更通知(觸發「 差異”階段)。然後,根據需要(和延遲),框架將通過遍歷反應式依賴樹來計算更改;每次更新變數時!這種對依賴狀態的「發現」需要付出巨大的代價......
透過「細粒度」反應性系統(如訊號),反應性變數/基元的更新會自動通知與它們相關的任何派生狀態的變化。因此,無需(重新)發現關聯的依賴關係;狀態傳播是有針對性的!
到 2024 年,大多數 Web 框架都選擇重新思考它們的工作方式,特別是在反應性模型方面。這種轉變總體上提高了他們的效率和競爭力。其他人選擇(仍然)混合(我在這裡考慮的是 Vue),這使他們在許多情況下更加靈活。
最後,無論選擇什麼模型,在我看來,一個(好的)反應式系統是建立在一些主要規則之上的:
最後一點可以解釋為聲明式程式設計的基本原則,這就是我如何看待(好的)反應式系統需要確定性!這就是使反應式模型可靠、可預測且易於在大規模技術專案中使用的“決定論”,無論演算法有多複雜。
以上是反應性是什麼鬼! ?的詳細內容。更多資訊請關注PHP中文網其他相關文章!