最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

談?wù)剰?fù)雜應(yīng)用的狀態(tài)管理(下):基于 Zustand 的漸進式狀態(tài)管理實踐

2022-12-29 16:01 作者:支付寶體驗科技  | 我要投稿

?????♀? 編者按:本文作者是螞蟻集團體驗設(shè)計師聞冰(社區(qū)稱呼:空谷),本篇將從具體使用的角度來詳細(xì)介紹下作者如何用?zustand 這一個狀態(tài)管理庫,來解決目前所遇到的所有狀態(tài)管理訴求。

銀彈存在嗎?

在上篇《為什么是 Zustand》中,我總結(jié)了在 ProEditor 這個重交互操作的場景下對狀態(tài)管理的訴求,本篇將會從具體使用的角度來詳細(xì)介紹下我是怎么用 zustand 這一個狀態(tài)管理庫,解決目前我所遇到的所有狀態(tài)管理訴求的。

首先想從云謙老師《數(shù)據(jù)流2022》最后的總結(jié)開始說起。

所以怎么選??沒有銀彈!如果站在社區(qū)開發(fā)者的角度來看。先看遠程狀態(tài)庫是否滿足需求,滿足的話就無需傳統(tǒng)數(shù)據(jù)流方案了;然后如果是非常非常簡單的場景用 useState + Context,復(fù)雜點的就不建議了,因為需要自行處理渲染優(yōu)化,手動把 Context 拆很細(xì)或者嘗試用 use-context-selector;再看心智模型,按內(nèi)部 store 或外部 store 區(qū)分選擇,個人已經(jīng)習(xí)慣后者,前者還在觀望;如果選外部 store,無兼容性要求的場景優(yōu)先用類 Valtio 基于 proxy 的方案,寫入數(shù)據(jù)時更符合直覺,同時擁有全自動的渲染優(yōu)化,有兼容性要求時則選 Zustand。

其實我對上述的這個決策方案并不是非常太認(rèn)同,原因是狀態(tài)管理會有一個重要但容易被忽略的核心需求:在遇到更加復(fù)雜的場景時,我們能不能用當(dāng)前的模式輕松地承接???例如:

  • 當(dāng)前只是 5 個狀態(tài),但業(yè)務(wù)突然加了一坨復(fù)雜的業(yè)務(wù)需求,然后要膨脹到15個狀態(tài)。這個時候狀態(tài)還容不容易維護?性能還能保證和之前一樣嗎?

  • 當(dāng)前是 React 環(huán)境,但突然業(yè)務(wù)說要加個 canvas 圖表的需求,圖表又有一些配置切換功能,還是用 React ,這個時候還能愉快地共享狀態(tài)么?

  • 當(dāng)前是個業(yè)務(wù)應(yīng)用,突然某個時候業(yè)務(wù)說要把這個應(yīng)用板塊抽成組件供外部復(fù)用,當(dāng)前的模式能不能輕松實現(xiàn)受控的模式改造成組件?(設(shè)想一下將語雀編輯器從應(yīng)用抽取變成組件)

如果承接不住,那么就意味著推翻重寫,這在我看來是不可接受的。理想的架構(gòu)選型,就應(yīng)該為可以預(yù)見的未來避開大部分坑,而不是遇到了再換一槍打一發(fā)。所以我自己做狀態(tài)管理庫決策選型的兩個核心原則是:

  1. 這個庫本身的 DX 好不好;

  2. 這個庫在未來一旦要遇到復(fù)雜場景的時候,能不能用簡單、低成本的方式兜住我的需求?

雖然我自己的實踐有限,但我還想說 zustand 是銀彈。

基于 Zustand 的漸進式狀態(tài)管理

最近在給 ProEditor 做 ProLayout 的可視化裝配器。其中一個很重要的編輯能力就是圖標(biāo)的選擇。而這個組件也存在一點點復(fù)雜度,剛好拿來作為 Zustand 用法的案例,可謂是「真·實戰(zhàn)案例」。首先簡單介紹一下圖標(biāo)選擇器這個組件。它的核心用途就是讓用戶可以快速選擇所需的圖標(biāo)。用戶可以選擇內(nèi)置的 Ant Design 的圖標(biāo),也可以使用 Iconfont 的圖標(biāo)。簡單的演示如下:

圖片
圖片

為了滿足上述的目標(biāo),這個組件具有下述功能:

  1. 展示圖標(biāo)列表;

  2. 選擇、刪除圖標(biāo);

  3. 搜索圖標(biāo);

  4. 切換 antd 圖標(biāo)和 iconfont 圖標(biāo)類目;

  5. 添加與刪除 Iconfont 腳本(暫不準(zhǔn)備加編輯);

  6. 切換 iconfont 腳本展示不同的 iconfont 腳本下的圖標(biāo);

同時,由于這個組件需要被多個場景復(fù)用,因此它需要支持非受控模式與受控模式。同時為了提升研發(fā)效率,我也希望能用 devtools 檢查相應(yīng)的狀態(tài)情況。

講完基礎(chǔ)的需求后,一起來看看這個組件是如何通過 Zustand 完整實現(xiàn)的。

Step 1: store 初始化 :State

首先拿最簡單的 tabs 切換做一個組件 tabs 切換的功能。新建一個?store.ts?文件,然后寫下如下代碼:

在相應(yīng)的組件(PickerPanel)中引入?useStore?,用 hooks 的方式即可解構(gòu)獲得?panelTabKey。而需要修改狀態(tài)時,可直接使用?useStore.setState?即可對?panelTabKey?進行修改。這樣, zustand 最簡單的狀態(tài)管理方法就完成了~

為了續(xù)統(tǒng)一心智,我在這里先將 create 中聲明的狀態(tài)部分,都稱為?State。由于 zustand 默認(rèn)全局單例,因此只要聲明一個 useStore 即可在所有地方使用,不用在外層套一個Context ,非常舒心。同時?useStore?又包含了一個?setState?的方法,因此在需要 React 中修改狀態(tài)時,可以直接使用?setState?進行狀態(tài)修改。這是 zustand 的最最簡單的使用方式,在場景初始化的時候,這樣就能直接上手使用,非常簡單,直接干掉 useStore + Context 妥妥的。

Step 2: 狀態(tài)變更方法:Action

在 Step 1 中,我們用?setState?來管理非常簡單的狀態(tài),這些狀態(tài)基本上用不著為其單獨設(shè)定相應(yīng)的變更功能。但是隨著業(yè)務(wù)場景的復(fù)雜性增多,我們不可避免地會遇到存在一次操作需要變更多個狀態(tài)的場景。:::info 而這些具有特定功能的狀態(tài)變更方法,我統(tǒng)一稱之為?Action。::: 在圖標(biāo)選擇器中,Action 其中之一的體現(xiàn)就是選擇圖標(biāo)的操作。選擇圖標(biāo)這個操作,除了設(shè)定當(dāng)前選中的圖標(biāo)以外,還需要關(guān)閉 popover、清除篩選關(guān)鍵詞(否則下次打開還是有篩選詞的)。

圖片

因此我們首先在 store中添加三個狀態(tài):

如果我們直接用 ?Step1 的方式,大致的寫法如下:

但此時會遇到新的問題,如果我在另外一個地方也需要使用這樣一段操作邏輯時,我要寫兩次么?當(dāng)然不,這既不利于開發(fā),也不利于維護。所以,在這里我們需要抽取一個?selectIcon?方法專門用于選擇圖標(biāo)這個操作,相關(guān)的狀態(tài)只要都寫在那里即可。而這就引出了狀態(tài)管理的第二步:自定義 Action。在?store.ts?中直接聲明并定義?selectIcon?函數(shù),然后第一個入?yún)⒏臑?set,就可以在 store.ts 的方法內(nèi)部直接修改狀態(tài)了,代碼如下所示:

對應(yīng)在?IconList?中,只需引入?selectIcon?方法即可。

另外值得一提的兩個小點:

  • Action 支持?async/await,直接給函數(shù)方法添加 async 符號即可;

  • zustand 默認(rèn)做了變量的優(yōu)化,只要是從?useStore解構(gòu)獲得的函數(shù),默認(rèn)是引用不變的,也就是使用 zustand store 的函數(shù)本身并不會造成不必要的重復(fù)渲染。

Step 3: 復(fù)雜狀態(tài)派生:Selector

在 Step2 中大家應(yīng)該有看到 iconList 這個狀態(tài),在上例中由于 iconList ?并不是重點,因此簡化了寫法。但事實上在圖標(biāo)選擇器組件中, iconList 并不是一個簡簡單單的狀態(tài),而是一個復(fù)合的派生狀態(tài)。在選擇器組件中, iconList 首先需要基于 是 Ant Design 的tab 或者 Iconfont 的 tabs 做原始圖標(biāo)數(shù)據(jù)源的進行切換,同時還需要支持相應(yīng)的檢索能力。而由于 Ant Design Tab 和 Iconfont 下的 list 具有不同的數(shù)據(jù)結(jié)構(gòu),因此篩選邏輯的實現(xiàn)也是不同的。

圖片

那在 zustand 的 Store 中,這個 iconList 是怎么實現(xiàn)的呢?在這里就要介紹 zustand 的又一個利器:Selector?。此 selector 和 redux 的 selector 的理念基本上是一致的,因此如果之前了解過 zustand 的 selector, zustand 的也一樣很容易理解。但從使用上來說,我認(rèn)為 zustand 的 selector 更加靈活易用。首先是定義 selector, selector 的入?yún)⑹峭暾?store (包含 state 和 action ),出參是目標(biāo)對象。

當(dāng)定義完成 selector 后,在組件層面作為 useStore 的第一個入?yún)⒓纯桑?/p>

如此一來,就完成了復(fù)雜狀態(tài)的派生實現(xiàn)。因為 useStore 可以像多個 hooks 一樣進行引入,因此我們就可以利用 selector 選出自己需要的各種狀態(tài),也可以多個 selector 間進行組合,復(fù)用通用邏輯。

圖片
圖片

另外,如果用 selector 選擇出來的變量也屬于 react 世界中的狀態(tài),因此為了避免不必要的重復(fù)渲染,可以對復(fù)雜的對象或者數(shù)組使用 isEqual 方法做比較,保證它的不變性。

最后,由于 selector 本身的定義只是個純函數(shù),也能非常方便地集成單元測試。

Step 4: 結(jié)構(gòu)組織與類型定義

經(jīng)過一部分功能開發(fā),一開始簡單的?store.ts?文件開始變得很長了,同時估計也開始遇到類型定義不準(zhǔn)確或找不到的情況了。那這對于后續(xù)項目的規(guī)?;l(fā)展非常不利,是時候做一次組織與整理了。

所以在我建議在 Step4 開始,就要對 Zustand 的 Store 進行更加合理地劃分。首先是從?store.ts?重構(gòu)為?store?文件夾,目錄結(jié)構(gòu)如下:

如此劃分的依據(jù)本質(zhì)上還是基于 State、Action 與 Selector 的三者切分:

  • initialState.ts:負(fù)責(zé) State —— 添加狀態(tài)類型與初始化狀態(tài)值;

  • createStore.ts:負(fù)責(zé)書寫創(chuàng)建 Store 的方法與 Action 方法;

  • selectors.ts:負(fù)責(zé) Selector ——派生類選擇器邏輯;

首先來看看?initialState?,這個文件中主要用于定于并導(dǎo)出后續(xù)在 Store 所有需要的狀態(tài)。導(dǎo)出的部分包含兩個:State?類型定義與 初始狀態(tài)?initialState。將 State 和 initialState 定義在一個文件中會有一個好處:類型跳轉(zhuǎn)會直接指向到這里,方便添加類型與類型的初始值。由于 state 單獨新建了一個文件,因此哪怕后續(xù)狀態(tài)再多,也能在這一個文件中看得清清楚楚。

再來看看?createStore?,這個文件由于包含了 Action 和 Store,會稍顯復(fù)雜一點,但是核心邏輯還是比較簡單的。

它做了這么幾件事:

  1. 定義了 store 中 Action 的類型,然后將 State 和 ?Action 合并為 Store 類型,并導(dǎo)出了 Store 的類型(比較重要);

  2. 給 create 方法添加了 Store 的類型,讓 store 內(nèi)部識別到自己這個 store 包含了哪些方法;

  3. 將?initialState?解構(gòu)導(dǎo)入 store(原來定義 state 的部分已經(jīng)抽出去到 initialState 里了);

  4. 在 useStore 里逐一實現(xiàn) Action 中定義的方法;

所以將從?store.ts?重構(gòu)到?createStore.ts?,基本上只有補充類型定義的工作量。

接下來再看下 selectors,這個文件很簡單,只需要導(dǎo)入 Store 的類型,然后逐一導(dǎo)出相應(yīng)的 selector 即可。


最后在?index.ts?中輸出相應(yīng)的方法和類型即可:

如此一來,我們通過 將 store.ts 單一職責(zé)的文件,拆分成各司其職的多個文件后,就初步解決了接下來可能的狀態(tài)大量擴展的問題與類型定義不準(zhǔn)確的問題,基本上可以保證項目的可維護性。

Step 5: 復(fù)雜 Action 交互:get()

Step1~Step3 可能在很大程度上就能滿足大部分場景的狀態(tài)管理訴求,從 Step4 開始,其實意味著狀態(tài)管理的復(fù)雜性開始上升,因此 Step4 也是為了駕馭復(fù)雜性而做的一個鋪墊。在圖標(biāo)選擇器這個組件中,ant design 的圖標(biāo)選擇部分并沒有太復(fù)雜的邏輯,最復(fù)雜的部分也只是關(guān)鍵詞搜索的展示。但 iconfont 部分的圖標(biāo)不同,這一部分由于存在用戶的輸入和多個數(shù)據(jù)源的配置、切換,復(fù)雜性是急劇上升的。譬如存在的展示狀態(tài)就有:空數(shù)據(jù) 、空數(shù)據(jù)但要添加數(shù)據(jù)源、有數(shù)據(jù)未選中態(tài)、有數(shù)據(jù)選中態(tài)等總共 7 種狀態(tài)。所以在狀態(tài)管理的復(fù)雜性也是急劇上升。

圖片

那 zustand 如何做到有效地收斂這些操作呢?核心思路就是基于用戶行為分層拆解一級承接的 Action ,最后在多個方法的基礎(chǔ)上統(tǒng)一抽取原子操作 Action。

圖片

首先是要識別用戶操作維度上的行為,在 Iconfont 這個場景下,用戶的行為有三類:添加數(shù)據(jù)源選擇數(shù)據(jù)源移除數(shù)據(jù)源。

  • 先來看「添加數(shù)據(jù)源」 :用戶感知到的添加數(shù)據(jù)源這個行為看起來簡單,但在數(shù)據(jù)流上其實包含四個步驟:? 顯示表單 ?-> ? 將數(shù)據(jù)源添加到數(shù)據(jù)源數(shù)組中 -> ? 隱藏表單-> ? 選中添加的數(shù)據(jù)源。

圖片
  • 再來看「選擇數(shù)據(jù)源」:選擇數(shù)據(jù)源就是當(dāng)用戶存在多個 Iconfont 圖標(biāo)源的時候,用戶可以按自己的訴求切換不同的 Iconfont 數(shù)據(jù)源;

圖片
  • 最后再看下「移除數(shù)據(jù)源」:移除數(shù)據(jù)源看似很簡單,但是其實也存在坑。即:移除當(dāng)前選中的數(shù)據(jù)源時,怎么處理選中態(tài)的邏輯?基于良好的用戶體驗考慮,我們會自動幫用戶切換一個選中的數(shù)據(jù)源。但這里就會有邊界問題:如果刪除的數(shù)據(jù)源第一個,那么應(yīng)該往后選擇,如果刪除的是最后一個數(shù)據(jù)源,那么應(yīng)該往前選擇。

圖片
  • |

圖片

那這樣的功能從數(shù)據(jù)流來看,移除數(shù)據(jù)源會包含三個階段:? 從數(shù)據(jù)源數(shù)組中移除相應(yīng)項 -> ? 決策需要選中哪個數(shù)據(jù)源 -> ? 選中相應(yīng)的數(shù)據(jù)源。

基于上述的分析,我們可以發(fā)現(xiàn)三層方法會存在一些重復(fù)的部分,主要是對數(shù)據(jù)源數(shù)組的更新與設(shè)定激活數(shù)據(jù)源。所以相應(yīng)的 Action 可以拆解如下:

圖片

所以我們在 store 中定義這些這些方法:

來看下具體的實現(xiàn),在 zustand 中能實現(xiàn)上述架構(gòu)的核心能力在于一個?get()?方法,能從自身中拿到所有的狀態(tài)(State & Action)。

當(dāng)完成相應(yīng)的功能實現(xiàn)后,只需要在相應(yīng)的觸發(fā)入口中添加方法即可。

基于這樣的一種模式,哪怕是這樣一個復(fù)雜的組件,在實現(xiàn)層面的研發(fā)心智仍然非常簡單:

  • React 層面仍然只是一個渲染層;

  • 復(fù)雜的狀態(tài)邏輯仍然以 hooks 式的模式進行引入;

  • 復(fù)雜的入口方法通過拆分子 Action 進行組合與復(fù)用;

而且我們在 Step2中已經(jīng)知道,在這種模式下,所有的 Action 都是默認(rèn)不需要包 useCallback 的~

Step 6: 從應(yīng)用邁向組件:Context 與 StoreUpdater

不知道有沒有小伙伴從 Step1 開始就在納悶,你不是說這是個組件嗎?為啥一直沒提受控模式呢?在你這個模式下,不是就變成全局單例了么?怎么搞成組件?那這一步不就來了么,就讓我們來看看在 zustand 模式下,一個已經(jīng)略顯復(fù)雜的應(yīng)用,如何輕輕松松變成一個受控的業(yè)務(wù)組件。因為 zustand 是默認(rèn)全局單例,所以如果需要變成組件,那么一定需要使用 Context 來隔離多個實例。而 這其中的關(guān)鍵,就是 zustand 提供的?createContext?方法。這個改造分為四步:

圖片

第一步:創(chuàng)建 Context 并添加 Provider先在?createStore.ts?下

第二步:創(chuàng)建并添加受控更新組件?StoreUpdater?

首先在組件入口處添加?StoreUpdater?組件。

那 StoreUpdater 具體是干什么的?看下面這張圖,我想大家就懂了。

圖片

簡單來說,就是通過?StoreUpdater?這個組件,做到外部 props 和內(nèi)部狀態(tài)的隔離。這樣一來,當(dāng)沒有外部props時,我們直接可以把這個 App 當(dāng)成普通應(yīng)用。而當(dāng)有外部的 props 時,可以通過?StoreUpdater?實現(xiàn)外部狀態(tài)的受控。利用這樣的思想,我們就可以很簡單地把一個 App 改造成受控的業(yè)務(wù)組件。這種架構(gòu)模式,也稱為「分形架構(gòu)」。

如果子組件能夠以同樣的結(jié)構(gòu),作為一個應(yīng)用使用,這樣的結(jié)構(gòu)就是分形架構(gòu)。在分形架構(gòu)下,每個應(yīng)用都可以變成組件,被更大的應(yīng)用合并消費。

具體來看看代碼:

在?StoreUpdater?這個組件中,核心分為三個部分:

  • useStoreUpdater?:將外部的 props 同步到 ?store 內(nèi)部的方法;

  • StoreUpdaterProps:從 store 的 State 中 pick 出需要受控的狀態(tài),并相應(yīng)補充 defaultXX 的 props;

  • StoreUpdater:逐一補充調(diào)用外部組件 props 的受控狀態(tài),將外部 props 更新到 store 的內(nèi)部狀態(tài)中;

針對受控組件來說,要實現(xiàn)一個狀態(tài) props 的受控,一定會有的孿生的兩個 props。例如一個 props 叫 value ,一定會有一個 defaultValue 和一個 onValueChange,才能滿足所有和這個value相關(guān)的場景訴求。因此我在?StoreUpdater?中是也完全基于這個規(guī)則書寫受控代碼。不過這里有個很有意思的點:就是在受控模式下,把 onChange 也當(dāng)成 store 的自持的狀態(tài)去思考。所有的 onChange 類方法,只有兩種狀態(tài)?null?和?function。這樣就能在 store 內(nèi)部很輕松地完成受控方法的集成。

而 zustand 在這個過程中發(fā)揮最關(guān)鍵一點的 hooks 叫做?useStoreApi。這個 props 可能大家在Step1~ Step5 中都沒看到過,因為這個 hooks 只在context 的場景下出現(xiàn)。它的功能就是獲得相應(yīng)?Context??下的?useStore?方法。如果直接使用?useStore,那么是獲得不到掛在在 useStore 上的變量的。大家是否還記得 Step1 中的?useStore.setState({ ... })這個方法?它在這個場景下發(fā)揮了巨大的作用,大大減少了更新受控 props 的代碼量。這是我們需要在這里使用?useStoreApi?的原因。

這一步寫完之后,組件接受外部 props ,并受控的部分就完成了。最后就只剩內(nèi)部狀態(tài)變更需要 onChange 出來了。

第三步:在相應(yīng)的 Action 里添加 onChange 方法在第二步中看到,我們需要在Store 的 State 中把 onChange 方法作為狀態(tài)自持,因此在 initalState 文件中,就需要補充相應(yīng)的類型定義和初始值:

而因為我們在 Step5 中通過收斂了一些原子級的 Action,基本做到了一個 State 有一個對應(yīng)的 Action,因此只需要相應(yīng)的 Action 處添加受控更新的 onChange 方法即可。

如此一來,組件的受控就完成了。

(可選)第四步:查找 useStore.setState 用法,補充 useStoreApi如果有一些狀態(tài)非常簡單,從寫下的一開始就始終是?useStore.setState?的寫法,那么這些寫法在組件化之后需要做一點點小調(diào)整。因為 useStore 是完全來自于 context 下的useStore,因此會丟失 setState 的相關(guān)方法。因此需要額外引入?useStoreApi?,并用 storeApi 來實施 setState。這可能算是算 zustand 從應(yīng)用遷移到組件的一點點小瑕疵。

不過如果是真正的復(fù)雜應(yīng)用,經(jīng)歷過 Step1~Step5 之后,估計大部分狀態(tài)變更都會收斂到 Store 中,因此如果需要修改 setState 的部分,在我實際使用下來并不算太多。最后來看下這樣的一個效果:

圖片

可以看到,基于 zustand 的這樣的開發(fā)模式,一個業(yè)務(wù)應(yīng)用可以非常簡單地遷移成為一個受控的業(yè)務(wù)組件,且任何一個需要對外暴露的數(shù)據(jù)源,都可以非常輕松地做到受控。而這是我認(rèn)為 zustand 作為狀態(tài)管理庫極其好用的一點。

PS:這個?StoreUpdater?的用法我也是翻了 react-flow 才了解到的。它的?StoreUpdater?比我這個組件可是多多了。而這樣的 props,使得 react-flow 這個復(fù)雜組件的靈活度和可玩性得到了保障。(有興趣的同學(xué)可以看看它的源碼 傳送門 ->)

Step 7: 性能優(yōu)化:又是 selector ?

作為一個制作精良的組件,性能上一定不能拉胯。因此我們需要來做下優(yōu)化了。最近云謙老師寫了篇《關(guān)于 React Re-Render》,可以看到如果要裸寫hooks做性能優(yōu)化,得學(xué)習(xí)一堆基礎(chǔ)知識和踩一堆坑,最后代碼里一坨一坨的 useXXX,心智負(fù)擔(dān)非常重。那在 zustand 中怎么做性能優(yōu)化呢?

首先,在 Step2 中我們已經(jīng)知道,所有 zustand 的 action 都默認(rèn)不會造成重復(fù)渲染,因此,理論上只有 state 會造成重復(fù)渲染。我們來看下實際情況。首先我使用?useWhyDidYouUpdate?的hooks來檢查并確認(rèn) PickerPanel 組件的 state 和 action 是否會會發(fā)變化,從下圖中可以看到,無論是 resetIcon 還是還是 storeApi ,它們的引用在其他狀態(tài)變化下,都保持不變,沒有造成重復(fù)渲染。

圖片

但在上圖中我們可以看到一個挺詭異的現(xiàn)象。就是明明?useWhyDidYouUpdate?已經(jīng)包含了所有的該組件使用的狀態(tài),在修改搜索關(guān)鍵詞時,State 都沒有變化,但是 Segment 那部分卻可以看到有重復(fù)渲染。那這是為什么?我們來改一下,寫法,將 store 從單獨定義為一個變量,然后用?useWhyDidYouUpdate?來檢查變更,可以看到下面這樣的情況:

圖片

即當(dāng)關(guān)鍵詞 state 修改了,就會造成 store 的變化。而 store 的變化就會觸發(fā)當(dāng)前這個界面的重新渲染。哎?那這個不就是和 context 一模一樣了么?對,你想的沒錯, zustand 并不像 valtio 這樣會自動收集依賴,并做性能優(yōu)化。所以我們是需要手動搞一輪優(yōu)化的。那咋搞呢?思路上其實也非常簡單:既然我在 PickerPanel 這個組件中只關(guān)心{ panelTabKey, icon, resetIcon }?這幾個狀態(tài),那我「手動」做一個依賴收集不就好了么?那怎么手動做呢?還記得 step2 中的 selector 嗎?又輪到它出場了。

圖片

只需要利用 zustand 的 useStore 的 selector 能力,配合 zustand 默認(rèn)提供的?shallow?淺比較能力。我們就能實現(xiàn)「人工的依賴收集」。如此一來,性能優(yōu)化也就做好了。我們來看看優(yōu)化前和優(yōu)化后的代碼區(qū)別:

可以看到,除了多一個幾乎一樣的 selector 和一個 shallow,其他代碼沒有任何區(qū)別,但是性能優(yōu)化就是這么做好了。那這是基于 zustand selector 的寫法可以做到的漸進式性能優(yōu)化?!感枰獌?yōu)化?加個 selector 就好~」這樣的研發(fā)心智,可以讓業(yè)務(wù)開發(fā)有很多選擇,譬如:

  • 前期撒開來默認(rèn)解構(gòu) useStore,不必?fù)?dān)心未來的性能優(yōu)化難題。等發(fā)現(xiàn)某些地方真的需要優(yōu)化時,相應(yīng)的套上 selector 就好;

  • 反正只是加個selector,也可以寫完應(yīng)用定型后也可以順手加一下;

  • 既然 selector 可以做優(yōu)化,那我干脆全部都直接?const x = useStore(s=>s.x),這樣引入好,也直接優(yōu)化完了。

Step 8: 研發(fā)增強:Devtools

當(dāng)Store復(fù)雜度到現(xiàn)在這樣之后,接下來每一步 debug 都有可能變得比較麻煩,因此我們可以集成一下 devtools,將 Store 研發(fā)模式變得更加可視化,做到可控。而寫法也非常簡單,只需在 create 方法下包一個 devtools 即可,并在create后多一個 () 執(zhí)行。

如此一來,我們就能夠使用 redux-dev-tools 可視化地查看 IconPicker 的數(shù)據(jù)流了。

圖片

不過大家可能會發(fā)現(xiàn),這個時候每一次的數(shù)據(jù)變更,都是是 anoymous 的變更說明,那有沒有可能讓每條變更都更加語義化呢?可以!只需在 set 方法的第三個參數(shù)中添加更新說明文本,就可以讓 devtools 識別到這項狀態(tài)變更。

還有嗎?

寫完上面 Step1~Step 8,基本上絕大多數(shù)狀態(tài)管理的需求就都能滿足了。但在一些各種邊界條件與復(fù)雜場景下,一定還是會有各種奇奇怪怪的訴求的。所以我在這里再列了一些自己 zustand 的其他用法,但不再細(xì)講:

  • 集成 redux ?reducer 或 react ?useReducer 的寫法;

  • 覺得事件處理麻煩,需要借用 rxjs 簡化事件流處理;

  • 需要結(jié)合一些請求庫,比如 swr 的使用方式,將 hooks 集成到 store 中;

  • 結(jié)合 persist 做本地數(shù)據(jù)緩存的方式;

  • 結(jié)合社區(qū)庫 zundo 簡單實現(xiàn)的一個歷史記錄功能;

  • 利用 subscribe 監(jiān)聽狀態(tài)變更,自動更新內(nèi)部狀態(tài);

  • 單一 store 的切片化;

  • 集成一些復(fù)雜三方庫(例如 y-js);

寫到這里,就基本上把這大半年所有基于 zustand 的狀態(tài)管理經(jīng)驗寫完了。希望對大家做狀態(tài)管理的相關(guān)決策有些幫助吧~

談?wù)剰?fù)雜應(yīng)用的狀態(tài)管理(下):基于 Zustand 的漸進式狀態(tài)管理實踐的評論 (共 條)

分享到微博請遵守國家法律
西充县| 信阳市| 萨迦县| 额尔古纳市| 镇江市| 龙海市| 海晏县| 公主岭市| 鞍山市| 天峻县| 苏尼特右旗| 蓝田县| 上栗县| 大港区| 灵宝市| 刚察县| 元朗区| 鹰潭市| 和平区| 保德县| 阿克陶县| 垣曲县| 美姑县| 阿鲁科尔沁旗| 祁东县| 调兵山市| 博野县| 鹰潭市| 基隆市| 涞源县| 隆林| 江阴市| 宾阳县| 衡东县| 襄城县| 泾阳县| 射洪县| 商城县| 辉县市| 普陀区| 洞口县|