第一階段(解釋)
TC39 提案冠軍:Daniel Ehrenberg、Yehuda Katz、Jatin Ramanathan、Shay Lewis、Kristen Hewell Garrett、Dominic Gannaway、Preston Sego、Milo M、Rob Eisenberg
原作者:羅布艾森伯格和丹尼爾埃倫伯格
本文檔描述了 JavaScript 中訊號的早期共同方向,類似於 ES2015 中 TC39 標準化 Promises 之前的 Promises/A+ 工作。使用 Polyfill 親自嘗試。
與 Promises/A+ 類似,這項工作的重點是協調 JavaScript 生態系統。如果這種協調成功,那麼就可以根據該經驗制定標準。幾個框架作者正在合作開發一個可以支持他們的反應性核心的通用模型。目前草案基於 Angular、Bubble、Ember、FAST、MobX、Preact、Qwik、RxJS、Solid、Starbeam、Svelte、Vue、Wiz 等作者/維護者的設計輸入...
與 Promises/A+ 不同,我們並不是試圖解決開發人員的通用表面 API,而是解決底層訊號圖的精確核心語意。該提案確實包含一個完全具體的 API,但該 API 並非針對大多數應用程式開發人員。相反,這裡的訊號 API 更適合建立在其之上的框架,透過通用訊號圖和自動追蹤機制提供互通性。
該提案的計劃是在進入第一階段之前進行重要的早期原型設計,包括整合到多個框架中。的好處 -提供的訊號。我們希望重要的早期原型設計能為我們提供這些資訊。更多詳情請參閱下方的「現況與發展計畫」。
為了開發複雜的使用者介面 (UI),JavaScript 應用程式開發人員需要以有效的方式儲存、運算、無效、同步狀態並將狀態推送到應用程式的視圖層。 UI 通常不僅涉及管理簡單的值,還經常涉及渲染計算狀態,該狀態依賴其他值的複雜樹或本身也計算的狀態。 Signals 的目標是提供用於管理此類應用程式狀態的基礎設施,以便開發人員可以專注於業務邏輯而不是這些重複的細節。
獨立發現類似訊號的構造在非 UI 上下文中也很有用,特別是在建置系統中以避免不必要的重建。
訊號用於反應式編程,以消除管理應用程式更新的需要。
用於根據狀態變更進行更新的聲明式程式設計模型。
來自什麼是反應性? 。
給定一個變數counter
,無論計數器是偶數還是奇數,您都希望將其渲染到 DOM 中。每當counter
發生變化時,您都希望使用最新的奇偶校驗來更新 DOM。在 Vanilla JS 中,你可能會遇到這樣的情況:
讓計數器 = 0;const setCounter = (值) => { 計數器=值; render();};const isEven = () => (counter & 1) == 0;const parity = () => isEven() ? "even" : "odd";const render = () => element.innerText = parity();// 模擬外部更新計數器...setInterval(() => setCounter(counter + 1), 1000);
這有很多問題...
counter
設定噪音大且樣板代碼繁重。
counter
狀態與渲染系統緊密耦合。
如果counter
發生變化但parity
沒有變化(例如計數器從 2 變為 4),則我們會進行不必要的奇偶校驗計算和不必要的渲染。
如果我們 UI 的另一部分只想在counter
更新時渲染呢?
如果我們 UI 的另一部分僅依賴isEven
或parity
怎麼辦?
即使在這個相對簡單的場景中,也會很快出現許多問題。我們可以嘗試透過為counter
引入 pub/sub 來解決這些問題。這將允許counter
的其他消費者可以訂閱以添加他們自己對狀態變化的反應。
然而,我們仍然面臨以下問題:
僅依賴parity
的渲染函數必須「知道」它實際上需要訂閱counter
。
如果不直接與counter
交互,則無法單獨基於isEven
或parity
來更新 UI。
我們增加了樣板文件。每當您使用某些東西時,這不僅僅是調用函數或讀取變數的問題,而是訂閱並在那裡進行更新的問題。管理取消訂閱也特別複雜。
現在,我們可以透過添加 pub/sub 來解決幾個問題,不僅可以用來counter
,還可以添加isEven
和parity
。然後,我們必須訂閱isEven
到counter
, parity
到isEven
,並render
到parity
。不幸的是,不僅我們的樣板程式碼爆炸了,而且我們還陷入了大量的訂閱簿記工作,如果我們不以正確的方式正確清理所有內容,則可能會導致記憶體洩漏災難。因此,我們解決了一些問題,但創建了全新的問題類別和大量程式碼。更糟的是,我們必須為系統中的每個狀態經歷整個過程。
儘管 JS 或 Web 平台中沒有內建任何此類機制,但模型和視圖 UI 中的資料綁定抽象長期以來一直是跨多種程式語言的 UI 框架的核心。在 JS 框架和庫中,已經進行了大量的不同方式的實驗來表示這種綁定,經驗已經證明了單向資料流與表示狀態或計算單元的第一類資料類型相結合的強大功能來自其他數據,現在通常稱為“信號”。這種一流的反應價值方法似乎在 2010 年的 Knockout 中首次流行於開源 JavaScript Web 框架中。在過去的 3-4 年裡,Signal 原語和相關方法獲得了進一步的關注,幾乎每個現代 JavaScript 函式庫或框架都以這樣或那樣的名稱提供類似的東西。
為了理解訊號,讓我們來看看上面的例子,它是用下面進一步闡述的訊號 API 重新想像的。
const counter = new Signal.State(0);const isEven = new Signal.Compulated(() => (counter.get() & 1) == 0);const parity = new Signal.Compulated(() => isEven .get() ? "even" : "odd");// 函式庫或框架基於其他Signal 原語定義效果effect(cb: () => void): (() => void);effect(( ) = > element.innerText = parity.get());// 模擬外部更新計數器...setInterval(() => counter.set(counter.get() + 1), 1000);
我們可以立即看到一些事情:
我們已經消除了前面範例中counter
變數周圍的吵雜樣板。
有一個統一的 API 來處理值、計算和副作用。
counter
和render
之間不存在循環引用問題或顛倒依賴關係。
無需手動訂閱,也無需記帳。
有一種方法可以控制副作用計時/調度。
訊號帶給我們的遠不止 API 表面所看到的:
自動依賴性追蹤- 計算訊號會自動發現它所依賴的任何其他訊號,無論這些訊號是簡單值還是其他計算。
惰性求值- 計算在聲明時不會立即求值,也不會在依賴項變更時立即求值。僅當明確請求其值時才會評估它們。
記憶化- 計算訊號快取其最後的值,以便其依賴項沒有變化的計算不需要重新計算,無論它們被存取多少次。
每個訊號實作都有自己的自動追蹤機制,以追蹤評估計算訊號時遇到的來源。這使得在不同框架之間共享模型、元件和函式庫變得困難——它們往往與視圖引擎存在錯誤耦合(考慮到訊號通常作為 JS 框架的一部分實現)。
該提案的目標是將反應式模型與渲染視圖完全解耦,使開發人員能夠遷移到新的渲染技術,而無需重寫其非UI 程式碼,或在JS 中開發共享反應式模型以部署在不同的上下文中。不幸的是,由於版本控制和重複,透過 JS 級庫達到強大的共享水平是不切實際的——內建函數提供了更強的共享保證。
由於內建了常用的庫,因此交付較少的程式碼始終會帶來很小的潛在效能提升,但訊號的實作通常非常小,因此我們預計這種影響不會很大。
我們懷疑訊號相關資料結構和演算法的原生 C++ 實作可能比 JS 中實現的效率稍微高出一個常數因子。然而,與 Polyfill 中存在的演算法相比,預計不會發生演算法變化;引擎在這裡並不需要魔法,而且反應性演算法本身將是明確定義的且明確的。
冠軍小組希望開發訊號的各種實現,並使用它們來研究這些性能的可能性。
使用現有的 JS 語言訊號庫,可能很難追蹤以下內容:
跨計算訊號鏈的呼叫堆疊,顯示錯誤的因果鏈
當一個訊號依賴另一個訊號時,訊號之間的參考圖-在調試記憶體使用情況時很重要
內建訊號使 JS 運行時和開發工具能夠改善對檢查訊號的支持,特別是調試或效能分析,無論這是內建在瀏覽器中還是透過共享擴展。可以更新元素檢查器、效能快照和記憶體分析器等現有工具,以在資訊呈現中專門突出顯示訊號。
一般來說,JavaScript 擁有相當小的標準庫,但 TC39 的趨勢是使 JS 更像是一種「自帶電池」的語言,提供高品質的內建功能集。例如,Temporal 正在取代 moment.js,許多小功能(例如Array.prototype.flat
和Object.groupBy
正在取代許多 lodash 用例。好處包括更小的套件大小、更高的穩定性和品質、加入新專案時需要學習的內容更少,以及 JS 開發人員普遍使用的通用詞彙。
W3C 和瀏覽器實作者目前的工作正在尋求將本機範本引入 HTML(DOM 元件和範本實例化)。此外,W3C Web 元件 CG 正在探索擴展 Web 元件以提供完全聲明性 HTML API 的可能性。為了實現這兩個目標,HTML 最終將需要一個響應式原語。此外,透過整合訊號對 DOM 進行許多人體工學改進是可以想像的,並且社區已經要求這樣做。
請注意,這種整合將是稍後的單獨工作,而不是該提案本身的一部分。
有時,即使瀏覽器不發生變化,標準化工作僅在「社區」層級就會有所幫助。 Signals 的工作將許多不同的框架作者聚集在一起,對反應性、演算法和互通性的本質進行深入討論。這已經很有用了,但並不能證明包含在 JS 引擎和瀏覽器中是合理的;只有當除了啟用生態系統資訊交換之外還有其他顯著優勢時,才應將訊號新增至 JavaScript 標準。
事實證明,現有的訊號庫在其核心方面彼此並沒有太大不同。該提案旨在透過實現其中許多圖書館的重要品質來鞏固他們的成功。
表示狀態的訊號類型,即可寫入訊號。這是其他人可以讀取的值。
計算/備忘錄/派生訊號類型,依賴其他訊號類型,並且延遲計算和快取。
計算是惰性的,這意味著當其依賴項之一發生更改時,預設不會再次計算計算訊號,而是僅在有人實際讀取它們時才運行。
計算是“無故障”的,這意味著不會執行不必要的計算。這意味著,當應用程式讀取計算訊號時,會對要運行的圖形的潛在髒部分進行拓撲排序,以消除任何重複項。
計算被緩存,這意味著如果在上次依賴項更改後,沒有依賴項發生更改,則在訪問時不會重新計算計算出的信號。
可以對計算訊號和狀態訊號進行自訂比較,以注意何時應更新依賴它們的進一步計算訊號。
對計算訊號具有其依賴項之一(或嵌套依賴項)的條件的反應會變得「髒」並發生變化,這意味著訊號的值可能已過時。
此反應旨在安排稍後執行更重要的工作。
效果是根據這些反應以及框架級調度來實現的。
計算訊號需要能夠對它們是否被註冊為這些反應之一的(嵌套)依賴項做出反應。
使 JS 框架能夠進行自己的調度。無 Promise 風格的內建強制調度。
需要同步反應才能根據框架邏輯安排後續工作。
寫入是同步的並且立即生效(批量寫入的框架可以在上面做到這一點)。
可以將檢查效果是否可能「髒」與實際運作效果分開(啟用兩階段效果排程器)。
能夠讀取訊號而不觸發要記錄的依賴關係( untrack
)
啟用使用訊號/反應性的不同程式碼庫的組合,例如,
就追蹤/反應性本身而言,一起使用多個框架(模數省略,見下文)
獨立於框架的反應式資料結構(例如,遞歸反應式儲存代理程式、反應式 Map 和 Set 和 Array 等)
阻止/禁止天真地濫用同步反應。
健全性風險:如果使用不當,可能會暴露「故障」:如果在設定訊號後立即完成渲染,則可能會向最終使用者暴露不完整的應用程式狀態。因此,此功能只能用於在應用程式邏輯完成後智慧地安排稍後的工作。
解決方案:禁止從同步反應回呼中讀取和寫入任何訊號
阻止untrack
並標記其不健全的本質
健全性風險:允許建立計算訊號,其值取決於其他訊號,但當這些訊號發生變化時不會更新。當未追蹤的訪問不會改變計算結果時應該使用它。
解決方案:該API在名稱中被標記為“不安全”。
注意:該提案確實允許從計算訊號和效果訊號中讀取和寫入訊號,而不限制讀取後的寫入,儘管存在健全性風險。做出這項決定是為了保持與框架整合的靈活性和相容性。
必須是多個框架實現其訊號/反應機制的堅實基礎。
應該是遞歸儲存代理程式、基於裝飾器的類別欄位反應性以及.value
和[state, setState]
樣式 API 的良好基礎。
語意能夠表達不同框架所支持的有效模式。例如,這些訊號應該有可能成為立即反映的寫入或稍後批次和應用的寫入的基礎。
如果 JavaScript 開發人員可以直接使用此 API,那就太好了。
想法:提供所有的鉤子,但如果可能的話,包括誤用時的錯誤。
想法:將微妙的 API 放在一個subtle
命名空間中,類似於crypto.subtle
,以標記API 之間的界限,這些API 是更高級的用法(例如實現框架或構建開發工具)與更日常的應用程式開發用法(例如實例化訊號以與框架。
然而,重要的是不要從字面上掩蓋完全相同的名字!
如果某個功能與生態系統概念相匹配,那麼使用通用詞彙會很好。
「JS 開發人員的可用性」和「提供框架的所有掛鉤」之間的緊張關係
可實現、可用且具有良好的效能—表面 API 不會造成太多開銷
啟用子類化,以便框架可以添加自己的方法和字段,包括私有字段。這對於避免在框架層面進行額外分配非常重要。請參閱下面的「記憶體管理」。
如果可能:如果沒有任何活動引用它以進行未來可能的讀取,則計算的訊號應該是垃圾收集的,即使它連結到保持活動狀態的更廣泛的圖(例如,透過讀取保持活動)。
請注意,今天的大多數框架都需要明確處理計算訊號,如果它們有任何對或來自另一個仍然存在的訊號圖的引用。
當它們的生命週期與 UI 元件的生命週期相關聯時,結果並沒有那麼糟糕,而且無論如何都需要處理效果。
如果使用這些語意執行成本太高,那麼我們應該將計算訊號的明確處理(或「取消連結」)新增到下面的 API,而該 API 目前還缺少它。
一個單獨的相關目標:最小化分配數量,例如,
製作一個可寫入訊號(避免兩個單獨的閉包+陣列)
實現效果(避免每個反應都關閉)
在用於觀察 Signal 變化的 API 中,避免建立額外的臨時資料結構
解決方案:基於類別的 API 可以重複使用子類別中定義的方法和字段
Signal API 的初步想法如下。請注意,這只是一個早期草案,我們預計會隨著時間的推移而改變。讓我們從完整的.d.ts
開始,了解整體形狀,然後我們將討論其含義的細節。
interface Signal<T> {// 取得訊號的值 get(): T;}namespace Signal {// 一個讀寫訊號類別 State<T> 實作 Signal<T> {// 建立一個以值開頭的狀態 Signal tconstructor (t: T, options?: SignalOptions<T>);// 取得訊號的值get(): T;// 設定狀態Signal 值為tset(t: T): void;}// 一個Signal這是一個基於其他Signalsclass 的公式Computed<T =known> 實作Signal<T> {// 建立一個訊號,其計算結果為回調回傳的值。 : Computed<T>) => T, options?: SignalOptions<T>);// 取得訊號的值get(): T;}// 此命名空間包含「進階」功能,最好// 留給框架作者而不是應用程式開發人員。目前計算的訊號,該訊號正在追蹤任何訊號讀取(如果有) null;// 傳回上次評估時所引用的所有訊號的有序列表。 ;// 傳回包含此訊號的觀察者,以及上次評估時讀取此訊號的任何計算訊號,// 如果該計算訊號被(遞歸地)監視。 | Watcher)[];// 如果此訊號是「即時」的,則為True,因為它由Watcher 監視,// 或由計算訊號讀取,該訊號是(遞歸地) ) live.function hasSinks(s : State | Computed): boolean;// 如果此元素是「反應性」的,則為True,因為它依賴// 某些其他訊號。 A Computed where hasSources is false // 將始終傳回相同的常數。自上次`watch`呼叫以來尚未呼叫它。集合,並設定觀察者在下次集合中的任何信號(或其依賴項之一)發生變化時運行其// 通知回調。以便/ / 將再次呼叫通知回呼。 ): void;// 傳回Watcher 集合中仍然髒的源集,或是一個計算訊號// 其源是髒的或掛起且尚未重新評估getPending(): Signal [];}// 觀察被觀看或不再觀看的鉤子var Watched: Symbol;var unwatched: Symbol;}interface SignalOptions<T> {// 新舊值之間的自訂比較函數。 Default: Object.is.// 訊號以 this 值傳入 context.equals?: (this: Signal<T>, t: T, t2: T) => boolean;// isWatched 變成時呼叫的回呼true,如果之前為false[Signal.subtle.watched]?: (this: Signal<T>) => void;// 每當isWatched 變成false 時調用的回調,如果之前為true[Signal.subtle.unwatched]? : (這: Signal<T>) => void;}}
訊號代表可能隨時間變化的資料單元。訊號可以是“狀態”(只是手動設定的值)或“計算的”(基於其他訊號的公式)。
計算訊號的工作原理是自動追蹤評估期間讀取的其他訊號。當讀取計算值時,它會檢查其先前記錄的任何依賴項是否已更改,如果已更改,則重新評估自身。當多個計算訊號嵌套時,追蹤的所有屬性都會轉到最裡面的一個。
計算訊號是惰性的,即基於拉的:它們僅在被存取時才重新評估,即使它們的依賴項之一較早發生了更改。
傳遞到計算訊號中的回調通常應該是“純粹的”,因為它是它訪問的其他訊號的確定性、無副作用的函數。同時,呼叫回調的時間是確定的,允許謹慎使用副作用。
訊號具有突出的快取/記憶功能:狀態訊號和計算訊號都會記住它們的當前值,並且只有在它們實際發生變化時才觸發引用它們的計算訊號的重新計算。甚至不需要重複比較舊值與新值——當來源訊號重置/重新評估時進行一次比較,訊號機制會追蹤引用該訊號的哪些內容尚未根據新值進行更新值還沒有。在內部,這通常透過「圖形著色」來表示,如(Milo 的部落格文章)所述。
計算訊號動態追蹤它們的依賴關係——每次運行它們時,它們最終可能依賴不同的事物,並且精確的依賴關係集在訊號圖中保持新鮮。這意味著,如果您只需要一個分支的依賴關係,並且先前的計算採用了另一個分支,那麼對暫時未使用的值的更改將不會導致計算出的Signal 被重新計算,即使在拉取時也是如此。
與 JavaScript Promises 不同,Signals 中的所有內容都是同步運作的:
將訊號設定為新值是同步的,並且在隨後讀取依賴該訊號的任何計算訊號時,這會立即反映出來。此突變沒有內建批次處理。
讀取計算訊號是同步的-它們的值始終可用。
如下所述,Watchers 中的notify
回呼在觸發它的.set()
呼叫期間同步運行(但在圖形著色完成之後)。
與 Promise 一樣,訊號可以表示錯誤狀態:如果計算訊號的回調拋出異常,則該錯誤將像另一個值一樣被緩存,並在每次讀取訊號時重新拋出。
Signal
實例表示讀取動態變化值的能力,該值的更新會隨著時間的推移而被追蹤。它還隱式地包括透過來自另一個計算訊號的追蹤來存取隱式地訂閱訊號的功能。
這裡的 API 旨在匹配大部分訊號庫在使用「訊號」、「計算」和「狀態」等名稱時達成的非常粗略的生態系統共識。但是,對計算訊號和狀態訊號的存取是透過.get()
方法進行的,這與所有流行的訊號 API 不同,這些 API 要么使用.value
樣式的存取器,要么使用signal()
呼叫語法。
此API旨在減少分配數量,使訊號適合嵌入JavaScript框架中,同時達到與現有框架自訂訊號相同或更好的效能。這意味著:
狀態訊號是單一可寫對象,可以從相同引用存取和設定它。 (請參閱下面“能力分離”部分中的含義。)
狀態和計算訊號都被設計為可子類化,以促進框架透過公共和私有類別欄位(以及使用該狀態的方法)添加附加屬性的能力。
使用相關訊號作為上下文的this
值來呼叫各種回呼(例如, equals
,計算的回調),因此不需要每個訊號一個新的閉包。相反,上下文可以保存在訊號本身的額外屬性中。
此 API 強制執行的一些錯誤條件:
遞歸讀取計算結果是錯誤的。
Watcher 的notify
回呼無法讀取或寫入任何訊號
如果計算訊號的回調拋出,則對該訊號的後續存取將重新拋出快取的錯誤,直到依賴項之一發生變更並重新計算。
一些不強制執行的條件:
計算訊號可以在其回調中同步寫入其他訊號
由觀察者的notify
回調排隊的工作可以讀取或寫入訊號,從而可以根據訊號複製經典的 React 反模式!
上面定義的Watcher
介面為實現典型 JS API 的效果提供了基礎:當其他訊號改變時,回呼會重新運行,純粹是為了它們的副作用。上面在初始範例中使用的effect
函數可以定義如下:
// 這個函數通常存在於函式庫/框架中,而不是應用程式程式碼中 // 注意:這個調度邏輯太基礎了,沒有什麼用處。不要複製/貼上。 w.getPending()) s.get();w.watch();});}});// 一個效果效果訊號,其計算結果為cb,它在微任務佇列上調度// 讀取自身每當其依賴項之一可能更改時導出函數effect(cb) {let destructor;let c = new Signal.Compulated(() => { destructor?.(); destructor = cb(); });w. watch(c) ;c.get();return () => { 析構函數? w.unwatch(c) };}
Signal API 不包含任何內建函數,例如effect
。這是因為效果調度很微妙,並且通常與框架渲染週期和 JS 無法存取的其他高階框架特定狀態或策略相關。
瀏覽此處使用的不同操作:傳遞給Watcher
建構函數的notify
回呼是當 Signal 從「乾淨」狀態(我們知道快取已初始化且有效)進入「已檢查」或「髒」狀態時調用的函數。 (其中快取可能有效也可能無效,因為該遞歸依賴的至少一個狀態已變更)。
對notify
的呼叫最終是透過在某些狀態訊號上呼叫.set()
來觸發的。此呼叫是同步的:它發生在.set
返回之前。但無需擔心此回調會觀察處於半處理狀態的訊號圖,因為在notify
回呼期間,即使在untrack
呼叫中,也無法讀取或寫入任何訊號。因為在.set()
期間呼叫了notify
,所以它正在中斷另一個可能不完整的邏輯執行緒。要從notify
讀取或寫入訊號,請安排工作稍後運行,例如,透過將訊號寫入清單中以供稍後訪問,或使用上面的queueMicrotask
。
請注意,完全可以透過調度計算訊號的輪詢來在沒有Symbol.subtle.Watcher
的情況下有效地使用訊號,就像 Glimmer 所做的那樣。然而,許多框架發現同步運行此調度邏輯通常很有用,因此 Signals API 包含了它。
計算訊號和狀態訊號都像任何 JS 值一樣被垃圾收集。但是觀察者有一種特殊的方式來保持事物的活動:只要任何底層狀態可達,觀察者觀察到的任何信號都將保持活動狀態,因為這些可能會觸發未來的notify
調用(然後是未來的.get()
)。因此,請記得調用Watcher.prototype.unwatch
來清理效果。
Signal.subtle.untrack
是一個逃生艙口,允許讀取訊號而不追蹤這些讀取。此功能是不安全的,因為它允許建立計算訊號,其值取決於其他訊號,但當這些訊號變更時不會更新。當未追蹤的訪問不會改變計算結果時應該使用它。
這些功能可能會在以後添加,但不包含在當前草案中。它們的省略是由於框架之間的設計空間缺乏既定的共識,以及透過本文檔中描述的訊號概念之上的機制來解決它們的缺失的能力。然而,不幸的是,這項遺漏限制了框架之間互通性的潛力。隨著本文檔中所述的訊號原型的產生,我們將努力重新檢查這些省略是否是適當的決定。
非同步:在此模型中,訊號始終同步可用於評估。然而,擁有某些導致訊號被設定的非同步過程以及了解訊號何時仍在「載入」通常很有用。對載入狀態進行建模的一種簡單方法是使用異常,並且計算訊號的異常快取行為在某種程度上合理地與該技術結合在一起。改進的技術將在第 30 期中討論。
事務:對於視圖之間的轉換,維護「from」和「to」狀態的即時狀態通常很有用。 「到」狀態在後台呈現,直到準備交換(進行交易),而「來自」狀態保持互動。同時維護這兩種狀態都需要「分叉」訊號圖的狀態,甚至可以一次支援多個待處理過渡可能很有用。第73期的討論。
也省略了一些可能的便利方法。
該提案是在2024年4月的TC39議程上為第1階段的議程。
提供了一些基本測試,可以提供該建議的多填充。一些框架作者已經開始嘗試替換該訊號實現,但是這種用法正處於早期階段。
信號提案的合作者希望特別保守地向我們推動該提案向前推進,以免我們陷入陷阱的陷阱,而這些陷阱最終會感到後悔而不是實際使用。我們的計劃是執行以下額外任務,而不是TC39流程所要求的,以確保此提案已步入正軌:
在提出第2階段之前,我們計劃:
開發多個生產級的多填充實現,這些實現是固體,經過良好測試的(例如,來自各種框架的通過測試以及Test262風格的測試),並且在性能方面具有競爭力(用徹底的信號/框架基準設定進行了驗證)。
將提出的訊號API整合到我們認為有些代表性的大量JS框架中,並且某些大型應用程式可用於此基礎。測試它在這些情況下是否有效,正確地工作。
對可能擴展到API的擴展空間有著深入的了解,並得出結論,該建議應添加到本提案中。
本節描述了暴露於JavaScript的每個API,以其實施的演算法。這可以將其視為一種原始規範,並在此早期被包括在內,以確定一組可能的語義,同時非常開放。
演算法的某些方面:
計算中訊號的讀取順序是重要的,並且可以觀察到某些回調(調用Watcher
, equals
, new Signal.Computed
已列出的第一個參數,並且執行了watched
/ unwatched
回調)。這意味著必須儲存計算訊號的來源。
這四個回調可能會引發異常,並且這些異常以可預測的方式傳播到呼叫JS程式碼。異常不會停止執行此演算法或將圖形留在半處理狀態。對於在觀察者的notify
回調中丟棄的錯誤,如果拋出了多個異常,則將異常發送到觸發它的.set()
呼叫。其他(包括watched
/ unwatched
?)儲存在訊號的值中,在讀取時會重新啟動,並且可以像其他任何具有正常值的人一樣標記這樣的~clean~
訊號。
在沒有「觀察」的計算訊號(任何觀察者觀察到)的計算訊號的情況下,請注意避免循環,以便可以將它們與訊號圖的其他部分獨立於垃圾收集。在內部,可以透過始終收集的生成編號系統來實現。請注意,最佳化的實作也可能包括局部每節點的產生編號,或避免在監視訊號上追蹤某些數字。
訊號演算法需要引用某些全球狀態。該狀態對於整個執行緒或“代理”都是全域的。
computing
:由於.get
或.run
調用或null
,目前正在重新評估的最內向或效應訊號。最初為null
。
frozen
:布林值表示目前是否有回調執行,該回呼要求不修改圖形。最初是false
。
generation
:從0開始的增量整數用於追蹤一個值的電流,同時避免循環。
Signal
名稱空間Signal
是一個普通對象,它是訊號相關類別和功能的命名空間。
Signal.subtle
是類似的內部名稱空間物件。
Signal.State
類Signal.State
內部插槽value
:狀態訊號的目前值
equals
:更改值時所使用的比較函數
watched
:當訊號透過效果觀察時要調用的回調
unwatched
:不再透過效果觀察訊號時要呼叫回呼
sinks
:一組依賴於此的手錶訊號
Signal.State(initialValue, options)
將此訊號的value
設為initialValue
。
設定此訊號equals
選項嗎?
watched
訊號設定為選項嗎?
將此訊號unwatched
選項?
將此訊號設為空sinks
Signal.State.prototype.get()
如果frozen
是真的,請引發例外。
如果computing
undefined
,請將此訊號新增至computing
的sources
集中。
注意:在觀察者觀看之前,我們不會在該訊號的sinks
器中加入computing
。
傳回此訊號的value
。
Signal.State.prototype.set(newValue)
如果當前的執行上下文被frozen
,請引發例外。
使用此訊號運行“設定訊號值”演算法和該值的第一個參數。
如果該演算法傳回~clean~
,則傳回未定義。
將該訊號的所有sinks
的state
設定為(如果是計算的訊號) ~dirty~
如果它們以前是清潔的,或者(如果是觀察者) ~pending~
如果以前是~watching~
。
將所有水槽的計算訊號依賴性(遞歸)的state
設定為~checked~
如果它們以前是~clean~
(也就是說,將骯髒的標記留在適當的位置),或者對於觀察者,則~pending~
在以前的~watching~
。
對於以前的每個~watching~
觀察者在該遞歸搜索中遇到的〜
將frozen
凍結為真。
呼叫他們的notify
回調(保存任何例外,但忽略了notify
的返回值)。
恢復frozen
的錯誤。
將觀察者的state
設定為~waiting~
。
如果從notify
回呼中拋出任何例外,請在所有notify
回呼運行後將其傳播給呼叫者。如果有多個例外,則將它們一起包裝成一個總體,然後將其丟棄。
返回未定義。
Signal.Computed
班級Signal.Computed
狀態機計算訊號的state
可能是以下一個:
~clean~
:訊號的值是存在的,並且已知不是陳舊的。
~checked~
:此訊號的(間接)來源已更改;該訊號具有一個值,但可能是陳舊的。只有在對所有直接來源進行評估時,才會知道是否陳舊。
~computing~
:當前,此訊號的回調是作為.get()
調用的副作用執行的。
~dirty~
:此訊號具有已知陳舊的值,或從未對其進行評估。
過渡圖如下:
狀態圖-v2
[*] - >髒
骯髒 - >計算:[4]
計算 - >清潔:[5]
乾淨 - >髒:[2]
清潔 - >檢查:[3]
檢查 - >清潔:[6]
檢查 - >髒:[1]
載入中過渡是:
數位 | 從 | 到 | 狀態 | 演算法 |
---|---|---|---|---|
1 | ~checked~ | ~dirty~ | 此訊號的直接來源是一個計算的訊號,已評估,其值已更改。 | 演算法:重新計算骯髒的計算訊號 |
2 | ~clean~ | ~dirty~ | 已設定了該訊號的直接來源,其值不等於其先前的值。 | 方法: Signal.State.prototype.set(newValue) |
3 | ~clean~ | ~checked~ | 已經設定了該訊號的遞歸(但不是直接的)來源,其值不等於其先前的值。 | 方法: Signal.State.prototype.set(newValue) |
4 | ~dirty~ | ~computing~ | 我們將執行callback 。 | 演算法:重新計算骯髒的計算訊號 |
5 | ~computing~ | ~clean~ | callback 已經完成評估,然後傳回了一個值或拋出異常。 | 演算法:重新計算骯髒的計算訊號 |
6 | ~checked~ | ~clean~ | 已經評估了該訊號的所有直接來源,並且所有這些訊號都沒有變化,因此我們現在眾所周知,我們並不陳舊。 | 演算法:重新計算骯髒的計算訊號 |
Signal.Computed
內部插槽value
:對於未讀取的計算訊號,訊號的先前的加速值或~uninitialized~
。值可能是個例外,當讀取該值時會重新啟動。始終undefined
效果訊號。
state
:可能是~clean~
, ~checked~
, ~computing~
或~dirty~
。
sources
:此訊號取決於的有序訊號集。
sinks
:一組取決於此訊號的訊號集。
equals
:選項中提供的等效方法。
callback
:呼叫以取得計算訊號的值的回調。將第一個參數設定為傳遞給建構函數。
Signal.Computed
構造函數構造函數集
callback
至其第一個參數
基於選項equals
預設為Object.is
state
到~dirty~
value
為~uninitialized~
借助Asynccontext,回呼傳遞給new Signal.Computed
Signal.Computed.prototype.get
如果目前的執行上下文已frozen
或該訊號具有狀態~computing~
,或者此訊號是效果併computing
計算訊號,請引發異常。
如果computing
不是null
,則將此訊號新增至computing
的sources
集中。
注意:我們不會將computing
加入到該訊號的sinks
中,直到/除非被觀察者觀看為止。
如果此訊號的狀態是~dirty~
或~checked~
:重複下列步驟,直到此訊號為~clean~
:
透過sources
進行反映,以找到最左右(即最早觀察到的)遞歸來源,它是標記~dirty~
的計算信號(在擊中一個~clean~
計算的信號時切斷搜索,並將此計算的信號包括作為最後一件事搜尋)。
在該訊號上執行「重新計算的計算訊號」演算法。
在這一點上,此訊號的狀態將是~clean~
,並且沒有遞歸來源是~dirty~
或~checked~
。傳回訊號的value
。如果值是例外,請重新歸因於該異常。
Signal.subtle.Watcher
類Signal.subtle.Watcher
狀態機觀察者的state
可能是以下一個:
~waiting~
: notify
回調已經運行,或者觀察者是新的,但沒有積極觀看任何信號。
~watching~
:觀察者正在積極觀看信號,但還沒有發生任何更改,這需要notify
回調。
~pending~
:觀察者的依賴性已經改變,但是尚未執行notify
回呼。
過渡圖如下:
狀態圖-v2
[*] - >等待
等待 - >觀看:[1]
觀看 - >等待:[2]
觀看 - >待定:[3]
待處理 - >等待:[4]
載入中過渡是:
數位 | 從 | 到 | 狀態 | 演算法 |
---|---|---|---|---|
1 | ~waiting~ | ~watching~ | 守望者的watch 方法已被稱為。 | 方法: Signal.subtle.Watcher.prototype.watch(...signals) |
2 | ~watching~ | ~waiting~ | 守望者的unwatch 方法已被調用,並刪除了最後的手錶訊號。 | 方法: Signal.subtle.Watcher.prototype.unwatch(...signals) |
3 | ~watching~ | ~pending~ | 手錶訊號可能已更改值。 | 方法: Signal.State.prototype.set(newValue) |
4 | ~pending~ | ~waiting~ | notify 回呼已運行。 | 方法: Signal.State.prototype.set(newValue) |
Signal.subtle.Watcher
內部插槽state
:可能是~watching~
, ~pending~
~waiting~
signals
:該觀察者正在觀看的有序訊號集
notifyCallback
:發生變化時呼叫的回呼。將第一個參數設定為傳遞給建構函數。
new Signal.subtle.Watcher(callback)
state
設定為~waiting~
。
將signals
初始化為一個空集。
notifyCallback
設定為回呼參數。
使用AsyncContext,回呼傳遞給了new Signal.subtle.Watcher
不會在調用建構函數的快照上關閉,因此可見圍繞寫入的上下文資訊。
Signal.subtle.Watcher.prototype.watch(...signals)
如果frozen
是真的,請引發例外。
如果任何參數不是訊號,請引發例外。
將所有參數附加到該物件signals
的末端。
對於每個新觀看的信號,以從左到右順序
將此觀察者加入該sink
。
如果這是第一個接收器,請重複到來源以添加該訊號作為水槽。
將frozen
凍結為真。
如果存在,請watched
回呼。
恢復frozen
的錯誤。
如果訊號的state
在~waiting~
,則將其設為~watching~
。
Signal.subtle.Watcher.prototype.unwatch(...signals)
如果frozen
是真的,請引發例外。
如果任何參數不是訊號,或沒有被該觀察者觀看,請引發例外。
對於參數中的每個訊號,從左到右順序,
從該觀察者的signals
設定中刪除該訊號。
從該訊號的sink
設定中刪除該觀察者。
如果該訊號的sink
集已為空,請從其每個來源中刪除該訊號作為水槽。
將frozen
凍結為真。
如果存在,請致電unwatched
回呼。
恢復frozen
的錯誤。
如果觀察者現在沒有signals
,且其state
為~watching~
,則將其設為~waiting~
。
Signal.subtle.Watcher.prototype.getPending()
傳回一個包含signals
集的數組,該訊號是在狀態下計算的訊號~dirty~
或~pending~
。
Signal.subtle.untrack(cb)
令c
為執行上下文的目前computing
狀態。
將computing
設為空。
致電cb
。
將computing
恢復為c
(即使cb
拋出了例外)。
傳回cb
的回傳值(復發任何例外)。
注意:Untrack並不能使您脫離嚴格的frozen
狀態。
Signal.subtle.currentComputed()
傳回目前的computing
值。
清除該訊號的sources
設置,並將其從這些來源的sinks
集中刪除。
儲存先前的computing
值並將computing
設定為此訊號。
將此訊號的狀態設為~computing~
。
使用此訊號作為此值來執行此計算訊號的回呼。儲存回傳值,如果回呼拋出了例外,請儲存以復發的儲存。
還原以前的computing
值。
將“設定訊號值”演算法應用於回調的返回值。
將此訊號的狀態設定為~clean~
。
如果演算法傳回~dirty~
:將該訊號的所有接收器標記為~dirty~
(以前,水槽可能是檢查和髒的混合物)。 (或者,如果未匹配,則採用新的一代數字來表示髒話或類似的東西。)
否則,該演算法會傳回~clean~
:在這種情況下,對於此訊號的每個~checked~
下沉,如果所有訊號的來源現在都乾淨,則也將該訊號標記為~clean~
。將此清理步驟應用於檢查水槽的任何新清潔訊號。 (或者,如果未匹配,則以某種方式表示相同的指示,以便清理可以懶惰地進行。)
如果透過此演算法通過一個值(與重新計算的髒訊號演算法相反,相對於復發的例外):
呼叫此訊號equals
函數,作為參數傳遞的當前value
,新值和此訊號。如果拋出異常,則將該異常(用於讀取時重新讀取)作為訊號的值,然後繼續,好像回調回傳false一樣。
如果該功能返回,則返回~clean~
。
將此訊號的value
設為參數。
返回~dirty~
Q :當他們剛開始成為2022年的熱門新事物時,標準化與訊號相關的內容是否很快?我們不應該給他們更多的時間發展和穩定嗎?
答:網路框架中訊號的當前狀態是超過10年持續發展的結果。隨著近年來投資的加劇,幾乎所有的網路框架都在接近非常相似的訊號模型。該建議是網路框架中許多當前領導者之間共享的設計練習的結果,如果沒有在各種情況下該組專家的驗證,它將不會推進標準化。
Q :鑑於框架與渲染和所有權的緊密整合,內建訊號甚至可以由框架使用嗎?
答:更特定於框架的零件往往屬於效果,調度和所有權/處置的領域,該提案沒有嘗試解決這些零件。我們使用原型標準標準訊號的首要任務是驗證他們可以相容且性能良好的現有框架「下方」。
Q :訊號API是要直接由應用程式開發人員使用還是由框架包裹?
答:雖然應用程式開發人員可以直接使用此API(至少不在Signal.subtle
內的零件。簡單的命名空間),但並非尤其是符合人體工學的。相反,圖書館/框架作者的需求是優先事項。預計大多數框架Signal.Computed
包覆基本的Signal.State
實際上,通常最好透過框架使用訊號,該框架可以管理更棘手的功能(例如,觀察者, untrack
),以及管理所有權和處置(例如,弄清楚何時應添加訊號並從觀察者那裡添加並刪除訊號),以及安排渲染到DOM-此提案不會試圖解決這些問題。
Q :當小部件被摧毀時,我是否必須拆除與小部件有關的信號?什麼是API?
答:這裡的相關拆卸操作是Signal.subtle.Watcher.prototype.unwatch
。只需清理手錶訊號(透過解開),而不受歡迎的訊號可以自動收集垃圾。
Q :訊號是否可以與vdom一起使用,還是直接與基礎HTML DOM一起使用?
答: 是的!訊號獨立於渲染技術。現有的JavaScript框架使用具有訊號構建體的構造(例如,preact),本機DOM(例如,實心)和組合(例如,VUE)。內建訊號也可以。
Q :在諸如Angular和Lit的基於類的框架的背景下,使用訊號是符合人體工學的嗎?像Svelte這樣的基於編譯器的框架呢?
答:可以使用簡單的訪問器裝飾器製成基於訊號的類別字段,如訊號polyfill readme所示。訊號與Svelte 5的符文非常緊密地對齊 - 編譯器可以簡單地將符文轉換為此處定義的訊號API,實際上,這是Svelte 5在內部使用的(但具有其自身的訊號庫)。
Q :訊號是否與SSR一起使用?保濕?恢復性?
答:是的。 Qwik使用這兩種屬性都使用訊號來良好效果,而其他框架則採用其他良好發達的水合方法,並具有不同的權衡訊號。我們認為,可以使用掛鉤的狀態和計算的訊號對Qwik的可重新訊號進行建模,並計劃以程式碼證明這一點。
Q :訊號是否像React一樣與單向資料流一起工作?
答:是的,訊號是單向資料流的機制。基於訊號的UI框架可讓您表示視圖作為模型的函數(模型包含訊號)。狀態和計算訊號的圖是通過構造的無環。還可以在訊號(!)中重新建立反應對抗逆轉錄,例如, useEffect
內部的setState
的訊號等效是使用觀察者安排寫入狀態訊號。
Q :訊號與Redux等狀態管理系統有何關係?訊號會鼓勵非結構化狀態嗎?
答:訊號可以為類似商店的狀態管理抽象構成有效的基礎。在多個框架中發現的一個常見模式是基於代理的對象,該對像在內部代表使用信號,例如,vue reactive()
或實心存儲的對象。這些系統可以在特定應用程式中以正確的抽象層級進行柔性狀態分組。
Q : Proxy
目前無法處理的訊號是什麼?
答:代理和訊號是互補的,並且融為一體。代理程式可讓您攔截淺物件操作和訊號協調一個依賴關係圖(儲存格)。用訊號支援代理是製造具有出色人體工學的嵌套反應性結構的好方法。
在此範例中,我們可以使用代理使訊號具有GETER和SETETER屬性,而不是使用get
和set
方法:
const a = new Signal.State(0); const b = new Proxy(a,{ get(目標,屬性,接收者){if(屬性==='value'){return target.get():} } set(目標,屬性,值,接收者){if(屬性==='value'){target.set(value)! }}); //在假設的反應性上下文中用法:<ememplate> {B.Value} <button onclick = {()=> {b.value ++; }}>更改</button> </template>
當使用針對細粒反應性進行最佳化的渲染器時,按一下該按鈕將導致b.value
儲存格更新。
看:
用訊號和代理程式創建的嵌套反應性結構的範例:訊號餐
範例的先驗實現,顯示了反應性資料ATD代理之間的關係:追蹤建構的INS
討論。
Q :訊號是基於推動還是基於拉的?
答:計算訊號的評估是基於拉的:僅在呼叫.get()
時評估計算訊號,即使基礎狀態更早發生了變化。同時,更改狀態訊號可能會立即觸發觀察者的回調,並「推」通知。因此,訊號可能被認為是“推扣”結構。
Q :訊號是否將非確定主義引入JavaScript執行?
答:否。在較高層次上,訊號遵循某些不變性的訊號,它們是「聲音」的。計算的訊號總是以一致的狀態觀察訊號圖,並且其執行不會被其他訊號雜音代碼打斷(除了其稱為本身的內容外)。請參閱上面的描述。
Q :當我寫入狀態訊號時,計算訊號的更新何時安排?
答:沒有安排!下次有人閱讀時,計算的訊號將自己重新計算。同步,可以呼叫觀察者的notify
回調,從而使框架在他們發現合適的時間安排讀取。
問:何時寫信給國家訊號生效?立即,還是批次?
答:將其寫入狀態訊號會立即反映 - 下一次取決於狀態訊號的計算訊號,即使需要,它也會重新計算自身,即使在遵循程式碼之後的立即進行。但是,這種機制固有的懶惰(僅在讀取時才計算出計算的訊號)意味著,在實踐中,計算可能以批次方式進行。
Q :訊號啟用「無故障」執行是什麼意思?
答:較早的基於推動的反應性模型面對冗餘計算問題:如果對狀態訊號的更新導致計算訊號熱切地運行,則最終可能會將更新推向UI。但是,如果要在下一幀之前對原始狀態訊號進行另一個更改,則將其寫入UI可能為時過早。有時,由於這種故障,不準確的中間值甚至顯示為最終用戶。訊號透過基於拉力而不是基於推動的訊號來避免這種動態:在框架計劃時,UI的渲染時間將拉動適當的更新,避免浪費在計算和書面上的浪費工作。
Q :訊號「有損」意味著什麼?
答:這是無故障執行的背面:訊號代表資料單元格 - 只是直接的電流值(可能會更改),而不是隨著時間的推移資料流。因此,如果您連續兩次寫信給狀態訊號,而無需做任何其他事情,則第一篇文字是「遺失」的,並且從未被任何計算的訊號或效果看到。這被理解為一個功能,而不是一個錯誤 - 其他構造(例如,非同步迭代,可觀察物)更適合流。
Q :本機訊號會比現有的JS訊號實現快嗎?
答:我們希望如此(以很小的不變因素)如此,但這在程式碼中尚待證明。 JS引擎不是魔術,最終將需要實現與JS訊號實現相同的演算法。請參閱上面的有關性能的部分。
Q :當對訊號的任何實際用法所必需的效果時,該建議為什麼不包含effect()
函數?
答:效果固有地與調度和處置連結在一起,這些效果由框架和本提案範圍之外的框架管理。相反,該建議包括透過更低級Signal.subtle.Watcher
實現效果的基礎。
Q :為什麼訂閱是自動的而不是提供手動介面?
答:經驗表明,反應性的手動訂閱介面是不存在的且容易出錯的。自動追蹤更為組合,是訊號的核心功能。
Q :為什麼Watcher
的回調同步運行,而不是在微型掩體中安排?
答:由於回呼無法讀取或寫入訊號,因此沒有透過同步稱呼它而帶來的不符合性。典型的回調會將訊號加到以後要讀取的陣列中,或在某個地方標記一些訊號。為所有這些操作做出單獨的微型遮罩是不必要的,而且不切實際。
Q :此API缺少我最喜歡的框架提供的一些好東西,這使得用訊號更容易編程。也可以加到標準中嗎?
答:也許。仍在考慮各種擴展。請提出一個問題,以提出有關您發現重要的任何缺失功能的討論。
Q :該API的大小或複雜度可以降低嗎?
答:這絕對是保持此API最小的目標,我們已經嘗試使用上面提出的目標來做到這一點。如果您對可以刪除的更多內容有想法,請提出一個要討論的問題。
問:我們不應該以更原始的概念(例如可觀察到的)開始在這一領域的標準化工作?
答:可觀察到某些事情可能是個好主意,但是它們並沒有解決旨在解決的問題的問題。如上所述,觀察到的或其他發布/訂閱機制並不是針對多種類型的UI編程的完整解決方案,這是因為對於開發人員而言過多容易出錯的配置工作,並且由於缺乏懶惰而浪費了工作,以及其他問題。
Q :鑑於大多數應用程式是基於網路的,為什麼在TC39而不是DOM中提出訊號?
答:該提案的某些合著者對非WEB UI環境感興趣,但是如今,隨著網路API在網路之外更頻繁地實施,這兩個場所都可能適合此目標。最終,訊號不需要依賴任何DOM API,因此無論哪種方式都可以使用。如果某人有很大的理由可以切換,請在問題中告訴我們。目前,所有貢獻者都簽署了TC39智慧財產權協議,該計劃是將其介紹給TC39。
Q :我可以使用標準訊號需要多長時間?
答:多填充已經可用,但是最好不要依靠它的穩定性,因為該API在其審核過程中演變。在幾個月或一年的時間裡,高品質,高性能穩定的多填充應該可用,但這仍然受委員會的修訂,而不是標準的。遵循TC39提案的典型軌跡,預計它將至少需要2 - 3年的時間,絕對最小值才能在所有瀏覽器中本來可以使用一些版本的信號,因此不需要多填充。
Q :就像{{JS/Web功能您不喜歡}}一樣,我們將如何防止標準化錯誤的訊號?
答:該提案計劃的作者在要求TC39的階段進步之前付出了原型的額外數量,並證明了事情。請參閱上面的「狀態和發展計畫」。如果您看到此計劃中的差距或改進機會,請提出解釋的問題。