從 Signals 看響應(yīng)式狀態(tài)管理
?????♀??編者按:本文作者是螞蟻集團(tuán)前端工程師新羅,介紹了響應(yīng)式狀態(tài)管理的一些理念和方法,希望能夠給大家?guī)?lái)一些參考。
什么是 Signals?
我們先從 Preact 作者 Jason Miller 發(fā)布的一篇文章說(shuō)起

Preact 引入了 Signals,提供了快速的響應(yīng)式狀態(tài)原語(yǔ)(或者叫原子吧),那么 Signals 有以下幾點(diǎn):
感覺(jué)上像是使用原始數(shù)據(jù)結(jié)構(gòu)
能根據(jù)值的變化自動(dòng)更新
直接更新 DOM (換句話來(lái)說(shuō)無(wú) VDOM)
沒(méi)有依賴數(shù)組
貼一下 Signals 的用法:
可以看到,跟?SolidJS?的?createSignal
非常相似,而且兩者有很多共同點(diǎn)(下面再說(shuō)),另外通過(guò).value
訪問(wèn)屬性非常類似于 Vue 中的 Ref。Signals 可以在一個(gè)應(yīng)用從小到大,在越來(lái)越復(fù)雜的邏輯迭代后,依然能保證性能。Singals 提供了細(xì)粒度狀態(tài)管理的好處,而無(wú)需通過(guò) memorize 或者其他 tricks 方式去優(yōu)化,Signals 跳過(guò)了數(shù)據(jù)在組件樹(shù)中的傳遞,而是直接更新所引用的組件。這樣開(kāi)發(fā)者就能降低使用心智,保證性能最佳。
下面是 hooks state 作為原子和 Signals 的性能比對(duì):
能達(dá)到如此表現(xiàn),Signals 有以下幾點(diǎn):
默認(rèn)惰性求值(lazy evaluate)- 只有被使用到的才會(huì)被監(jiān)聽(tīng)和更新
最佳更新策略
最佳依賴追蹤策略 - 不像 hooks 需要指定依賴
直接訪問(wèn)狀態(tài)值,不需要 selector 或其他 hooks
另外從上面可以看出 Signals 是可以獨(dú)立于組件外的,跟 hooks 方式不一樣
那么是不是 Signals 既可以服務(wù)于 Preact,也能集合到其他框架中呢?從 Signals 目標(biāo)中,我們看到了這點(diǎn):

而且目前,Signals 可以單獨(dú)品嘗使用,不用依附 UI 框架。比如:https://codesandbox.io/s/tender-burnell-7c7g9n?file=/src/index.js
其他框架
我們把目光切換到 SolidJS ,先看一下 SolidJS 響應(yīng)式數(shù)據(jù)例子:
可以看到 SolidJS 響應(yīng)式也是也 Signal 作為基礎(chǔ),createSignal?
既可以用于組件內(nèi),也可以用于組件外,這個(gè)跟 Preact 中類似。一方面可以將 Signal 作為組件的 local state,也可以定義為 global State。與前面類似,SolidJS 中也有以下相似點(diǎn):
響應(yīng)式細(xì)粒度更新
無(wú)需定義 dependencies
惰性取值
SolidJS 與 Mobx 和 Vue 的響應(yīng)式非常相似,但是不會(huì)處理 VDOM,而是直接更新 DOM。所以 SolidJS 的性能表現(xiàn)也比較不錯(cuò):
這里不再介紹 Vue 的響應(yīng)式,有興趣的可以再去了解下
響應(yīng)式狀態(tài)管理三要素
信號(hào): Signals
這里其實(shí)很難翻譯,一個(gè)基礎(chǔ)響應(yīng)式數(shù)據(jù)怎么命名,這里用 Signals 先代替,當(dāng)然我們有可能會(huì)看到比較叫:Observables(Mobx等),Atoms(Recoil,Jotai等),Refs(Vue等)。不過(guò)基本意思都一樣,表示一個(gè)響應(yīng)式數(shù)據(jù)的單元。
這里取值用的是 function,有些地方用的是?.value
,意味著也可以通過(guò) Object 的 getter, setter 或者 Proxy 去進(jìn)行數(shù)據(jù)處理
反應(yīng): Reactions
反應(yīng)這里很好理解,大部分地方叫 Effect ,也就是副作用,當(dāng)然也有用 actions 的,下方是一個(gè)基本例子:
反應(yīng)也就是在數(shù)據(jù)更新時(shí)的監(jiān)聽(tīng)器,作為響應(yīng)式數(shù)據(jù)的基礎(chǔ),也是必不可少的一環(huán)
衍生: Derivations
這里是指數(shù)據(jù)的衍生狀態(tài),本質(zhì)上也可以認(rèn)為是 Signals 的變種,常見(jiàn)命令可能有 computed, memo 等。
衍生能緩存計(jì)算結(jié)果,避免重復(fù)的計(jì)算,并且也能自動(dòng)追蹤依賴以及同步更新。
響應(yīng)式特點(diǎn)
響應(yīng)式數(shù)據(jù)管理會(huì)存儲(chǔ)不同節(jié)點(diǎn)之間的鏈接關(guān)系,當(dāng)每次節(jié)點(diǎn)更新之后,會(huì)重新檢查鏈接關(guān)系。如果不在關(guān)聯(lián),就會(huì)解綁鏈接,取消依賴。
下方的例子更能體現(xiàn):
運(yùn)行效果可見(jiàn):https://codesandbox.io/s/tender-burnell-7c7g9n?file=/src/index.js
另外響應(yīng)式還有一點(diǎn)就是同步更新,同步更新避免了狀態(tài)不一致的問(wèn)題(相信使用React的同學(xué)深有感受),也提高了更好的預(yù)測(cè)性和可測(cè)試性。在響應(yīng)式數(shù)據(jù)更新的基礎(chǔ)上,有些也會(huì)加入比如批量更新,批量更新在避免重復(fù)執(zhí)行反應(yīng)和衍生上大有好處,大大避免了一些多余額外的執(zhí)行消耗
手動(dòng)實(shí)現(xiàn)一個(gè)
響應(yīng)式狀態(tài)管理核心還是用的觀察者模式,當(dāng) Signals 更新時(shí),Reactions 會(huì)訂閱到數(shù)據(jù)變化從而更新數(shù)據(jù)。
Signals
首先實(shí)現(xiàn)一個(gè)基礎(chǔ)的數(shù)據(jù)更新與讀取
輸出結(jié)果:

加上訂閱邏輯,重新更改下:
從上述可以看到,在讀取的時(shí)候會(huì)獲取當(dāng)前執(zhí)行的上下文,拿到 Reactions 的方法,并且方法依賴?yán)镌黾赢?dāng)前Signals,這樣 Reactions 就能訂閱到這個(gè) Signals,當(dāng) Signals 更新時(shí),會(huì)執(zhí)行所包含的訂閱方法。接下來(lái)我們把 Reactions 補(bǔ)充下
Reactions
廢話不多說(shuō),直接看代碼
但是這里有個(gè)問(wèn)題就是,隨著 Reactions 每次執(zhí)行,running 的 deps 會(huì)逐步累加,所以需要在執(zhí)行前,清空deps。
這樣 Reactions 也就 OK 了
Derivations
那么 Derivations 的代碼就簡(jiǎn)單多了
其實(shí)也很好理解,如前面所說(shuō),衍生是一種特殊的 Signals,所以直接返回 Signal,另外 Reactions 是可以追蹤訂閱到 Signals 的變化,所以在 Reactions 函數(shù)里設(shè)置 Derivations 的值就可以了。
完整 Demo 見(jiàn):https://codesandbox.io/s/elastic-blackwell-br79m2?file=/src/index.js
通過(guò)短短不到 100 行的代碼就能實(shí)現(xiàn)一個(gè)基礎(chǔ)的細(xì)粒度更新的狀態(tài)管理(當(dāng)然我們這里用的是方法去取值,也可以用 proxy 等方式,例如 valtio 等),但是僅僅這些到了實(shí)際應(yīng)用和跟 UI 框架融合還是不夠的,需要有更多的完善和補(bǔ)充。如何將響應(yīng)式代碼融入到渲染過(guò)程,可以參考這篇文章(https://indepth.dev/posts/1289/solidjs-reactivity-to-rendering)。也可以看看 Signals 是如何從實(shí)際代碼上融入到 Preact 與 React 中的(https://github.com/preactjs/signals/tree/main/packages)。
回到 React
讓我們把目光回到 React 上,從目前來(lái)看,React 的狀態(tài)管理有很多,可以見(jiàn)云謙老師的這個(gè)文章,不同的框架對(duì)比如下:

這樣看來(lái) valtio 更符合 Signals 在 React中的實(shí)現(xiàn)
無(wú)需 dependency
外部 store,無(wú)需關(guān)聯(lián)組件
但是作為 React 相關(guān)的框架,最后都會(huì)去做 VDOM 更新,跟 SolidJS 直接更新不一樣,當(dāng)然我們也期望 React 官方能做一些改變?nèi)?yōu)化現(xiàn)有的開(kāi)發(fā)體驗(yàn),比如:React Forget/ useEvent
最后
這篇文章并不是想指導(dǎo)如何做狀態(tài)管理的選型,也不是去分析不同狀態(tài)管理的優(yōu)劣。只是介紹響應(yīng)式狀態(tài)管理的一些理念和方法,希望能對(duì)大家有一些價(jià)值參考。
引用
https://preactjs.com/blog/introducing-signals
https://preactjs.com/guide/v10/signals/
https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf
https://indepth.dev/posts/1289/solidjs-reactivity-to-rendering
https://cn.vuejs.org/guide/extras/reactivity-in-depth.html
https://my5353.com/gCocL
https://mp.weixin.qq.com/s/26_yYH5fbDyMTEKOMcNxtA