對于“前端狀態(tài)”相關(guān)問題,如何思考比較全面
大家好,我卡頌。
最近看到個寫得很不錯的知乎回答Hooks是否過譽了?前端應該跟著React走還是跟著JS、TS走?- beeplin的回答[1]。
在這個回答的基礎(chǔ)上,我想引申出一個問題 —— 對于「前端狀態(tài)」相關(guān)問題,如何思考比較全面?
今天,我們試著從多個抽象層級的角度回答這個問題。
問題的起源
有相當比例的前端從業(yè)者入行是從「學習前端框架的使用」開始的。換言之,在他們的知識體系中,最底層是「前端框架如何使用」,其他業(yè)務知識都是構(gòu)建于此之上。
要以此為基礎(chǔ)回答「前端狀態(tài)」相關(guān)問題,并不容易。就比如你問組長:
- 為什么項目中用Redux而不用Mobx?
- 為什么要用Hooks而不用ClassComponent?
很多時候得到的是一個既定的事實(就是這樣,沒有為什么),而不是分析后的結(jié)果。
要分析這類問題,我們需要知道一些更低抽象層級的知識。
幾乎所有主流前端框架的實現(xiàn)原理,都在踐行UI = f(state)這個公式,通俗的說 —— 「UI是對狀態(tài)的映射」。
這應該是「前端狀態(tài)」會出現(xiàn)的最低抽象層級了,所以我們從這個層級出發(fā)。
前端框架的實現(xiàn)原理
限于篇幅有限,這里我們以最常見的React與Vue舉例。
在實現(xiàn)「UI是對狀態(tài)的映射」過程中,兩者的方向不同。
React?并不關(guān)心狀態(tài)如何變化。每當調(diào)用更新狀態(tài)的方法(比如this.setState?,或者useState dispatch?...),就會對整個應用進行diff。
所以在React中,傳遞給「更新狀態(tài)的方法」的,是「狀態(tài)的快照」,換言之,是個「不可變的數(shù)據(jù)」。
Vue?關(guān)心狀態(tài)如何變化。每當更新狀態(tài)時,都會對「與狀態(tài)關(guān)聯(lián)的組件」進行diff。
所以在Vue中,是直接改變狀態(tài)的值。換言之,狀態(tài)是個「可變的數(shù)據(jù)」。
這種底層實現(xiàn)的區(qū)別在單獨使用框架時不會有很大區(qū)別,但是會影響上層庫的實現(xiàn)(比如狀態(tài)管理庫)。
現(xiàn)在我們知道,通過前端框架,我們可以將狀態(tài)映射到UI。那么如何管理好對應的映射關(guān)系呢?
換言之,如何將狀態(tài)與「和他相關(guān)的UI」約束在一起?
我們再往更高一級抽象看。
如何封裝組件
前端開發(fā)普遍采用「組件」作為「狀態(tài)與UI的松散耦合單元」。
到這里我們可以發(fā)現(xiàn),如果僅僅會使用前端框架,那么只能將組件看作是「前端框架中既定的設(shè)計」。
但如果從更低一層抽象(前端框架的實現(xiàn)原理)出發(fā),就能發(fā)現(xiàn) —— 組件是為了解決框架實現(xiàn)原理中「UI到狀態(tài)的映射」的途徑。
那么組件該如何實現(xiàn),他的載體是什么呢?從軟件工程的角度出發(fā),有兩個方向可以探索:
- 面向?qū)ο缶幊?/li>
- 函數(shù)式編程
「面向?qū)ο缶幊獭沟奶攸c包括:
- 繼承
- 封裝
- 多態(tài)
其中「封裝」這一特點使得「面向?qū)ο缶幊獭购茏匀怀蔀榻M件的首選實現(xiàn)方式,畢竟組件的本質(zhì)就是「將狀態(tài)與UI封裝在一起的松散耦合單元」。
React的ClassComponent,Vue的Options API都是類似實現(xiàn)。
但畢竟組件的本質(zhì)是「狀態(tài)與UI的松散耦合單元」,在考慮復用性時,不僅要考慮「邏輯的復用」(邏輯是指操作狀態(tài)的業(yè)務代碼),還要考慮「UI的復用」。所以「面向?qū)ο缶幊獭沟牧韮蓚€特性并不適用于組件。
框架們根據(jù)自身特點,在「類面向?qū)ο缶幊獭沟慕M件實現(xiàn)上,拓展了復用性:
- React?通過HOC、renderProps
- Vue2?通過mixin
經(jīng)過長期實踐,框架們逐漸發(fā)現(xiàn) —— 「類面向?qū)ο缶幊痰慕M件實現(xiàn)」中「封裝」帶來的好處不足以抵消「復用性」上的劣勢。
于是React?引入了Hooks?,以函數(shù)作為組件封裝的載體,借用「函數(shù)式編程」的理念提高復用性。類似的還有Vue3?中的Composition API。
不管是ClassComponent?還是FunctionComponent?、Options API?還是Composition API,他們的本質(zhì)都是「狀態(tài)與UI的松散耦合單元」。
當組件數(shù)量增多,邏輯變復雜時,一種常見的解耦方式是 —— 將可復用的邏輯從組件中抽離出來,放到單獨的Model?層。UI?直接調(diào)用Model層的方法。
對Model層的管理,也就是所謂的「狀態(tài)管理」。
對狀態(tài)的管理,是比組件中「狀態(tài)與UI的耦合」更高一級的抽象。
狀態(tài)管理問題
「?狀態(tài)管理」要考慮的最基本的問題是 —— 如何與框架實現(xiàn)原理盡可能契合?
比如,我們要設(shè)計一個User Model?,如果用class的形式書寫:
只需要將這個Model的實例包裝為響應式對象,就能很方便的接入Vue3:
之所以這么方便,誠如本文開篇提到的 —— Vue?的實現(xiàn)原理中,狀態(tài)是「可變的數(shù)據(jù)」,這與User Model的用法是契合的。
同樣的User Model?要接入React?則比較困難,因為React原生支持的是「不可變數(shù)據(jù)」類型的狀態(tài)。
要接入React?,我們可以將同樣的User Model?設(shè)計為不可變數(shù)據(jù),采用reducer的形式書寫:
如果一定要接入「可變類型狀態(tài)」,可以為React?提供類似Vue?的「響應式更新」能力后再接入。比如借用Mobx提供的響應式能力:
到目前為止,不管是「可變類型狀態(tài)」還是「不可變類型狀態(tài)」的Model,都帶來了「從組件中抽離邏輯」的能力,對于上例來說:
- 「可變類型狀態(tài)」將狀態(tài)與邏輯抽離到User中
- 「不可變類型狀態(tài)」將狀態(tài)與邏輯抽離到userModel與userReducer
- 最終暴露給UI的都僅僅是changeName方法
當業(yè)務進一步復雜,Model本身需要更完善的架構(gòu),此時又是更高一級的抽象。
到這一層時已經(jīng)脫離前端框架的范疇,上升到純狀態(tài)的管理,比如為mobx?帶來結(jié)構(gòu)化數(shù)據(jù)的mobx-state-tree。
此時框架實現(xiàn)原理對Model?的影響已經(jīng)在更高的抽象中被抹去了,比如Redux-toolkit?是React?技術(shù)棧的解決方案,Vuex?是Vue技術(shù)棧的解決方案,但他們在使用方式上是類似的。
這是因為Redux與Vuex的理念都借鑒自Flux,即使React與Vue在實現(xiàn)原理上有區(qū)別,但這些區(qū)別都被狀態(tài)管理方案抹平了。
更高的抽象
在此之上,對于狀態(tài)還有沒有更高的抽象呢?答案是肯定的。
對于常規(guī)的狀態(tài)管理方案,根據(jù)用途不同,可以劃分出更多細分領(lǐng)域,比如:
- 對于表單狀態(tài),收斂到表單狀態(tài)管理庫中。
- 對于服務端緩存,收斂到服務端狀態(tài)管理庫中(React Query、SWR)。
- 用完整的框架收斂前后端Model,比如Remix、Next.js。
總結(jié)
回到我們開篇提到的問題:
- 為什么項目中用Redux而不用Mobx?
- 為什么要用Hooks而不用ClassComponent?
現(xiàn)在我們已經(jīng)能清晰的知道這兩個問題的相同點與不同點:
- 相同點:都與狀態(tài)相關(guān)。
- 不同點:屬于不同抽象層級的狀態(tài)相關(guān)問題。
要回答這些問題需要哪些知識呢?只需要知道問題涉及的「狀態(tài)的抽象層級」,以及「比該層級更低的抽象層級」對應的知識即可。
比如回答:為什么項目中用Redux?而不用Mobx?
考慮當前抽象層級
Redux與Mobx?都屬于Model?的實現(xiàn),前者帶來一套「類Flux的狀態(tài)管理理念」,后者為React?帶來「響應式更新」能力,在設(shè)計Model時我的項目更適合哪種類型?
或者兩種類型我都不在乎,那么要不要使用更高抽象的解決方案(比如MST、Redux Toolkit)抹平這些差異?
考慮低一級抽象層級
項目用的ClassComponent還是FunctionComponent?Redux、Mobx與他們結(jié)合使用時哪個組合更能協(xié)調(diào)好UI與邏輯的松散耦合?
考慮再低一級抽象層級
React?的實現(xiàn)原理決定了他原生與「不可變類型狀態(tài)」更親和。Redux?更契合「不可變數(shù)據(jù)」,Mobx更契合「可變數(shù)據(jù)」。我的項目需要考慮這些差異么?
當了解不同抽象層級需要考慮的問題后,任何寬泛的、狀態(tài)相關(guān)問題都能轉(zhuǎn)化成具體的、多抽象層級問題。
從不同抽象層級出發(fā)思考,就能更全面的回答問題。
參考資料
[1]Hooks是否過譽了?前端應該跟著React走還是跟著JS、TS走?- beeplin的回答:https://www.zhihu.com/question/468249924/answer/1968728853。