大家好,我卡頌。
最近看到個寫得很不錯的知乎回答Hooks是否過譽了?前端應該跟着React走還是跟着JS、TS走?- beeplin的回答。
在這個回答的基礎上,我想引申出一個問題 —— 對於前端狀態相關問題,如何思考比較全面?
今天,我們試着從多個抽象層級的角度回答這個問題。
歡迎加入人類高質量前端框架羣,帶飛
問題的起源
有相當比例的前端從業者入行是從學習前端框架的使用開始的。換言之,在他們的知識體系中,最底層是前端框架如何使用,其他業務知識都是構建於此之上。
要以此為基礎回答前端狀態相關問題,並不容易。就比如你問組長:
- 為什麼項目中用
Redux而不用Mobx? - 為什麼要用
Hooks而不用ClassComponent?
很多時候得到的是一個既定的事實(就是這樣,沒有為什麼),而不是分析後的結果。
要分析這類問題,我們需要知道一些更低抽象層級的知識。
幾乎所有主流前端框架的實現原理,都在踐行UI = f(state)這個公式,通俗的説 —— UI是對狀態的映射。
這應該是前端狀態會出現的最低抽象層級了,所以我們從這個層級出發。
前端框架的實現原理
限於篇幅有限,這裏我們以最常見的React與Vue舉例。
在實現UI是對狀態的映射過程中,兩者的方向不同。
React並不關心狀態如何變化。每當調用更新狀態的方法(比如this.setState,或者useState dispatch...),就會對整個應用進行diff。
所以在React中,傳遞給更新狀態的方法的,是狀態的快照,換言之,是個不可變的數據。
Vue關心狀態如何變化。每當更新狀態時,都會對與狀態關聯的組件進行diff。
所以在Vue中,是直接改變狀態的值。換言之,狀態是個可變的數據。
這種底層實現的區別在單獨使用框架時不會有很大區別,但是會影響上層庫的實現(比如狀態管理庫)。
現在我們知道,通過前端框架,我們可以將狀態映射到UI。那麼如何管理好對應的映射關係呢?
換言之,如何將狀態與和他相關的UI約束在一起?
我們再往更高一級抽象看。
如何封裝組件
前端開發普遍採用組件作為狀態與UI的鬆散耦合單元。
到這裏我們可以發現,如果僅僅會使用前端框架,那麼只能將組件看作是前端框架中既定的設計。
但如果從更低一層抽象(前端框架的實現原理)出發,就能發現 —— 組件是為了解決框架實現原理中UI到狀態的映射的途徑。
那麼組件該如何實現,他的載體是什麼呢?從軟件工程的角度出發,有兩個方向可以探索:
- 面向對象編程
- 函數式編程
面向對象編程的特點包括:
- 繼承
- 封裝
- 多態
其中封裝這一特點使得面向對象編程很自然成為組件的首選實現方式,畢竟組件的本質就是將狀態與UI封裝在一起的鬆散耦合單元。
React的ClassComponent,Vue的Options API都是類似實現。
但畢竟組件的本質是狀態與UI的鬆散耦合單元,在考慮複用性時,不僅要考慮邏輯的複用(邏輯是指操作狀態的業務代碼),還要考慮UI的複用。所以面向對象編程的另兩個特性並不適用於組件。
框架們根據自身特點,在類面向對象編程的組件實現上,拓展了複用性:
React通過HOC、renderPropsVue2通過mixin
經過長期實踐,框架們逐漸發現 —— 類面向對象編程的組件實現中封裝帶來的好處不足以抵消複用性上的劣勢。
於是React引入了Hooks,以函數作為組件封裝的載體,借用函數式編程的理念提高複用性。類似的還有Vue3中的Composition API。
不管是ClassComponent還是FunctionComponent、Options API還是Composition API,他們的本質都是狀態與UI的鬆散耦合單元。
當組件數量增多,邏輯變複雜時,一種常見的解耦方式是 —— 將可複用的邏輯從組件中抽離出來,放到單獨的Model層。UI直接調用Model層的方法。
對Model層的管理,也就是所謂的狀態管理。
對狀態的管理,是比組件中狀態與UI的耦合更高一級的抽象。
狀態管理問題
狀態管理要考慮的最基本的問題是 —— 如何與框架實現原理儘可能契合?
比如,我們要設計一個User Model,如果用class的形式書寫:
class User {
name: String;
constructor(name: string) {
this.name = name;
}
changeName(name: string) {
return this.name = name;
}
}
只需要將這個Model的實例包裝為響應式對象,就能很方便的接入Vue3:
import { reactive } from 'vue'
setup() {
const user = reactive(new User('KaSong') as User;
return () => (
<button onClick={() => user.changeName('XiaoMing')}>
{user.name}
</button>
)
}
之所以這麼方便,誠如本文開篇提到的 —— Vue的實現原理中,狀態是可變的數據,這與User Model的用法是契合的。
同樣的User Model要接入React則比較困難,因為React原生支持的是不可變數據類型的狀態。
要接入React,我們可以將同樣的User Model設計為不可變數據,採用reducer的形式書寫:
const userModel = {
name: 'KaSong'
};
const userReducer = (state, action) => {
switch (action.type) {
case "changeName":
const name = action.payload;
return {...state, name}
}
};
function App() {
const [user, dispatch] = useReducer(userReducer, userModel);
const changeName = (name) => {
dispatch({type: "changeName", payload: name});
};
return (
<button onClick={() => changeName('XiaoMing')}>
{user.name}
</button>
);
}
如果一定要接入可變類型狀態,可以為React提供類似Vue的響應式更新能力後再接入。比如借用Mobx提供的響應式能力:
import { makeAutoObservable } from "mobx"
function createUser(name) {
return makeAutoObservable(new User(name));
}
到目前為止,不管是可變類型狀態還是不可變類型狀態的Model,都帶來了從組件中抽離邏輯的能力,對於上例來説:
- 可變類型狀態將狀態與邏輯抽離到
User中 - 不可變類型狀態將狀態與邏輯抽離到
userModel與userReducer - 最終暴露給
UI的都僅僅是changeName方法
當業務進一步複雜,Model本身需要更完善的架構,此時又是更高一級的抽象。
到這一層時已經脱離前端框架的範疇,上升到純狀態的管理,比如為mobx帶來結構化數據的mobx-state-tree。
此時框架實現原理對Model的影響已經在更高的抽象中被抹去了,比如Redux-toolkit是React技術棧的解決方案,Vuex是Vue技術棧的解決方案,但他們在使用方式上是類似的。
這是因為Redux與Vuex的理念都借鑑自Flux,即使React與Vue在實現原理上有區別,但這些區別都被狀態管理方案抹平了。
更高的抽象
在此之上,對於狀態還有沒有更高的抽象呢?答案是肯定的。
對於常規的狀態管理方案,根據用途不同,可以劃分出更多細分領域,比如:
- 對於表單狀態,收斂到表單狀態管理庫中
- 對於服務端緩存,收斂到服務端狀態管理庫中(
React Query、SWR) - 用完整的框架收斂前後端
Model,比如Remix、Next.js
總結
回到我們開篇提到的問題:
- 為什麼項目中用
Redux而不用Mobx? - 為什麼要用
Hooks而不用ClassComponent?
現在我們已經能清晰的知道這兩個問題的相同點與不同點:
- 相同點:都與狀態相關
- 不同點:屬於不同抽象層級的狀態相關問題
要回答這些問題需要哪些知識呢?只需要知道問題涉及的狀態的抽象層級,以及比該層級更低的抽象層級對應的知識即可。
比如回答:為什麼項目中用Redux而不用Mobx?
考慮當前抽象層級
Redux與Mobx都屬於Model的實現,前者帶來一套類Flux的狀態管理理念,後者為React帶來響應式更新能力,在設計Model時我的項目更適合哪種類型?
或者兩種類型我都不在乎,那麼要不要使用更高抽象的解決方案(比如MST、Redux Toolkit)抹平這些差異?
考慮低一級抽象層級
項目用的ClassComponent還是FunctionComponent?Redux、Mobx與他們結合使用時哪個組合更能協調好UI與邏輯的鬆散耦合?
考慮再低一級抽象層級
React的實現原理決定了他原生與不可變類型狀態更親和。Redux更契合不可變數據,Mobx更契合可變數據。我的項目需要考慮這些差異麼?
當了解不同抽象層級需要考慮的問題後,任何寬泛的、狀態相關問題都能轉化成具體的、多抽象層級問題。
從不同抽象層級出發思考,就能更全面的回答問題。