對(duì)于“前端狀態(tài)”相關(guān)問(wèn)題,如何思考比較全面


大家好,我卡頌。

最近看到個(gè)寫得很不錯(cuò)的知乎回答Hooks是否過(guò)譽(yù)了?前端應(yīng)該跟著React走還是跟著JS、TS走?- beeplin的回答[1]。

在這個(gè)回答的基礎(chǔ)上,我想引申出一個(gè)問(wèn)題 —— 對(duì)于「前端狀態(tài)」相關(guān)問(wèn)題,如何思考比較全面?

今天,我們?cè)囍鴱亩鄠€(gè)抽象層級(jí)的角度回答這個(gè)問(wèn)題。

問(wèn)題的起源
有相當(dāng)比例的前端從業(yè)者入行是從「學(xué)習(xí)前端框架的使用」開始的。換言之,在他們的知識(shí)體系中,最底層是「前端框架如何使用」,其他業(yè)務(wù)知識(shí)都是構(gòu)建于此之上。

要以此為基礎(chǔ)回答「前端狀態(tài)」相關(guān)問(wèn)題,并不容易。就比如你問(wèn)組長(zhǎng):

為什么項(xiàng)目中用Redux而不用Mobx?

為什么要用Hooks而不用ClassComponent?

很多時(shí)候得到的是一個(gè)既定的事實(shí)(就是這樣,沒有為什么),而不是分析后的結(jié)果。

要分析這類問(wèn)題,我們需要知道一些更低抽象層級(jí)的知識(shí)。

幾乎所有主流前端框架的實(shí)現(xiàn)原理,都在踐行UI = f(state)這個(gè)公式,通俗的說(shuō) —— 「UI是對(duì)狀態(tài)的映射」。

這應(yīng)該是「前端狀態(tài)」會(huì)出現(xiàn)的最低抽象層級(jí)了,所以我們從這個(gè)層級(jí)出發(fā)。

前端框架的實(shí)現(xiàn)原理
限于篇幅有限,這里我們以最常見的React與Vue舉例。

在實(shí)現(xiàn)「UI是對(duì)狀態(tài)的映射」過(guò)程中,兩者的方向不同。

React并不關(guān)心狀態(tài)如何變化。每當(dāng)調(diào)用更新狀態(tài)的方法(比如this.setState,或者useState dispatch...),就會(huì)對(duì)整個(gè)應(yīng)用進(jìn)行diff。

所以在React中,傳遞給「更新狀態(tài)的方法」的,是「狀態(tài)的快照」,換言之,是個(gè)「不可變的數(shù)據(jù)」。

Vue關(guān)心狀態(tài)如何變化。每當(dāng)更新狀態(tài)時(shí),都會(huì)對(duì)「與狀態(tài)關(guān)聯(lián)的組件」進(jìn)行diff。

所以在Vue中,是直接改變狀態(tài)的值。換言之,狀態(tài)是個(gè)「可變的數(shù)據(jù)」。

這種底層實(shí)現(xiàn)的區(qū)別在單獨(dú)使用框架時(shí)不會(huì)有很大區(qū)別,但是會(huì)影響上層庫(kù)的實(shí)現(xiàn)(比如狀態(tài)管理庫(kù))。

現(xiàn)在我們知道,通過(guò)前端框架,我們可以將狀態(tài)映射到UI。那么如何管理好對(duì)應(yīng)的映射關(guān)系呢?

換言之,如何將狀態(tài)與「和他相關(guān)的UI」約束在一起?

我們?cè)偻咭患?jí)抽象看。

如何封裝組件
前端開發(fā)普遍采用「組件」作為「狀態(tài)與UI的松散耦合單元」。

到這里我們可以發(fā)現(xiàn),如果僅僅會(huì)使用前端框架,那么只能將組件看作是「前端框架中既定的設(shè)計(jì)」。

但如果從更低一層抽象(前端框架的實(shí)現(xiàn)原理)出發(fā),就能發(fā)現(xiàn) —— 組件是為了解決框架實(shí)現(xiàn)原理中「UI到狀態(tài)的映射」的途徑。

那么組件該如何實(shí)現(xiàn),他的載體是什么呢?從軟件工程的角度出發(fā),有兩個(gè)方向可以探索:

面向?qū)ο缶幊?br>
函數(shù)式編程

「面向?qū)ο缶幊獭沟奶攸c(diǎn)包括:

繼承

封裝

多態(tài)

其中「封裝」這一特點(diǎn)使得「面向?qū)ο缶幊獭购茏匀怀蔀榻M件的首選實(shí)現(xiàn)方式,畢竟組件的本質(zhì)就是「將狀態(tài)與UI封裝在一起的松散耦合單元」。

React的ClassComponent,Vue的Options API都是類似實(shí)現(xiàn)。

但畢竟組件的本質(zhì)是「狀態(tài)與UI的松散耦合單元」,在考慮復(fù)用性時(shí),不僅要考慮「邏輯的復(fù)用」(邏輯是指操作狀態(tài)的業(yè)務(wù)代碼),還要考慮「UI的復(fù)用」。所以「面向?qū)ο缶幊獭沟牧韮蓚€(gè)特性并不適用于組件。

框架們根據(jù)自身特點(diǎn),在「類面向?qū)ο缶幊獭沟慕M件實(shí)現(xiàn)上,拓展了復(fù)用性:

React通過(guò)HOC、renderProps

Vue2通過(guò)mixin

經(jīng)過(guò)長(zhǎng)期實(shí)踐,框架們逐漸發(fā)現(xiàn) —— 「類面向?qū)ο缶幊痰慕M件實(shí)現(xiàn)」中「封裝」帶來(lái)的好處不足以抵消「復(fù)用性」上的劣勢(shì)。

于是React引入了Hooks,以函數(shù)作為組件封裝的載體,借用「函數(shù)式編程」的理念提高復(fù)用性。類似的還有Vue3中的Composition API。

不管是ClassComponent還是FunctionComponent、Options API還是Composition API,他們的本質(zhì)都是「狀態(tài)與UI的松散耦合單元」。

當(dāng)組件數(shù)量增多,邏輯變復(fù)雜時(shí),一種常見的解耦方式是 —— 將可復(fù)用的邏輯從組件中抽離出來(lái),放到單獨(dú)的Model層。UI直接調(diào)用Model層的方法。

對(duì)Model層的管理,也就是所謂的「狀態(tài)管理」。

對(duì)狀態(tài)的管理,是比組件中「狀態(tài)與UI的耦合」更高一級(jí)的抽象。

狀態(tài)管理問(wèn)題
「狀態(tài)管理」要考慮的最基本的問(wèn)題是 —— 如何與框架實(shí)現(xiàn)原理盡可能契合?

比如,我們要設(shè)計(jì)一個(gè)User Model,如果用class的形式書寫:

class User {
  name: String;
  constructor(name: string) {
  this.name = name;
 }
 changeName(name: string) {
  return this.name = name;
 }
}
只需要將這個(gè)Model的實(shí)例包裝為響應(yīng)式對(duì)象,就能很方便的接入Vue3:

import { reactive } from 'vue'

setup() {
  const user = reactive(new User('KaSong') as User;
  return () => (
   <button onClick={() => user.changeName('XiaoMing')}>
      {user.name}
    </button>
  )
}
之所以這么方便,誠(chéng)如本文開篇提到的 —— Vue的實(shí)現(xiàn)原理中,狀態(tài)是「可變的數(shù)據(jù)」,這與User Model的用法是契合的。

同樣的User Model要接入React則比較困難,因?yàn)镽eact原生支持的是「不可變數(shù)據(jù)」類型的狀態(tài)。

要接入React,我們可以將同樣的User Model設(shè)計(jì)為不可變數(shù)據(jù),采用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>
  );
}
如果一定要接入「可變類型狀態(tài)」,可以為React提供類似Vue的「響應(yīng)式更新」能力后再接入。比如借用Mobx提供的響應(yīng)式能力:

import { makeAutoObservable } from "mobx"

function createUser(name) {
    return makeAutoObservable(new User(name));
}
到目前為止,不管是「可變類型狀態(tài)」還是「不可變類型狀態(tài)」的Model,都帶來(lái)了「從組件中抽離邏輯」的能力,對(duì)于上例來(lái)說(shuō):

「可變類型狀態(tài)」將狀態(tài)與邏輯抽離到User中

「不可變類型狀態(tài)」將狀態(tài)與邏輯抽離到userModel與userReducer

最終暴露給UI的都僅僅是changeName方法

當(dāng)業(yè)務(wù)進(jìn)一步復(fù)雜,Model本身需要更完善的架構(gòu),此時(shí)又是更高一級(jí)的抽象。

到這一層時(shí)已經(jīng)脫離前端框架的范疇,上升到純狀態(tài)的管理,比如為mobx帶來(lái)結(jié)構(gòu)化數(shù)據(jù)的mobx-state-tree。

此時(shí)框架實(shí)現(xiàn)原理對(duì)Model的影響已經(jīng)在更高的抽象中被抹去了,比如Redux-toolkit是React技術(shù)棧的解決方案,Vuex是Vue技術(shù)棧的解決方案,但他們?cè)谑褂梅绞缴鲜穷愃频摹?br>
這是因?yàn)镽edux與Vuex的理念都借鑒自Flux,即使React與Vue在實(shí)現(xiàn)原理上有區(qū)別,但這些區(qū)別都被狀態(tài)管理方案抹平了。

更高的抽象
在此之上,對(duì)于狀態(tài)還有沒有更高的抽象呢?答案是肯定的。

對(duì)于常規(guī)的狀態(tài)管理方案,根據(jù)用途不同,可以劃分出更多細(xì)分領(lǐng)域,比如:

對(duì)于表單狀態(tài),收斂到表單狀態(tài)管理庫(kù)中

對(duì)于服務(wù)端緩存,收斂到服務(wù)端狀態(tài)管理庫(kù)中(React Query、SWR)

用完整的框架收斂前后端Model,比如Remix、Next.js

總結(jié)
回到我們開篇提到的問(wèn)題:

為什么項(xiàng)目中用Redux而不用Mobx?

為什么要用Hooks而不用ClassComponent?

現(xiàn)在我們已經(jīng)能清晰的知道這兩個(gè)問(wèn)題的相同點(diǎn)與不同點(diǎn):

相同點(diǎn):都與狀態(tài)相關(guān)

不同點(diǎn):屬于不同抽象層級(jí)的狀態(tài)相關(guān)問(wèn)題

要回答這些問(wèn)題需要哪些知識(shí)呢?只需要知道問(wèn)題涉及的「狀態(tài)的抽象層級(jí)」,以及「比該層級(jí)更低的抽象層級(jí)」對(duì)應(yīng)的知識(shí)即可。

比如回答:為什么項(xiàng)目中用Redux而不用Mobx?

考慮當(dāng)前抽象層級(jí)
Redux與Mobx都屬于Model的實(shí)現(xiàn),前者帶來(lái)一套「類Flux的狀態(tài)管理理念」,后者為React帶來(lái)「響應(yīng)式更新」能力,在設(shè)計(jì)Model時(shí)我的項(xiàng)目更適合哪種類型?

或者兩種類型我都不在乎,那么要不要使用更高抽象的解決方案(比如MST、Redux Toolkit)抹平這些差異?

考慮低一級(jí)抽象層級(jí)
項(xiàng)目用的ClassComponent還是FunctionComponent?Redux、Mobx與他們結(jié)合使用時(shí)哪個(gè)組合更能協(xié)調(diào)好UI與邏輯的松散耦合?

考慮再低一級(jí)抽象層級(jí)
React的實(shí)現(xiàn)原理決定了他原生與「不可變類型狀態(tài)」更親和。Redux更契合「不可變數(shù)據(jù)」,Mobx更契合「可變數(shù)據(jù)」。我的項(xiàng)目需要考慮這些差異么?

當(dāng)了解不同抽象層級(jí)需要考慮的問(wèn)題后,任何寬泛的、狀態(tài)相關(guān)問(wèn)題都能轉(zhuǎn)化成具體的、多抽象層級(jí)問(wèn)題。

從不同抽象層級(jí)出發(fā)思考,就能更全面的回答問(wèn)題。

參考資料
[1]
Hooks是否過(guò)譽(yù)了?前端應(yīng)該跟著React走還是跟著JS、TS走?- beeplin的回答:
https://www.zhihu.com/question/468249924/answer/1968728853


作者:卡頌


歡迎關(guān)注微信公眾號(hào) :魔術(shù)師卡頌