對於“前端狀態”相關問題,如何思考比較全面

卡頌發表於2022-12-23

大家好,我卡頌。

最近看到個寫得很不錯的知乎回答Hooks是否過譽了?前端應該跟著React走還是跟著JS、TS走?- beeplin的回答

在這個回答的基礎上,我想引申出一個問題 —— 對於前端狀態相關問題,如何思考比較全面?

今天,我們試著從多個抽象層級的角度回答這個問題。

歡迎加入人類高質量前端框架群,帶飛

問題的起源

有相當比例的前端從業者入行是從學習前端框架的使用開始的。換言之,在他們的知識體系中,最底層是前端框架如何使用,其他業務知識都是構建於此之上。

要以此為基礎回答前端狀態相關問題,並不容易。就比如你問組長:

  • 為什麼專案中用Redux而不用Mobx
  • 為什麼要用Hooks而不用ClassComponent

很多時候得到的是一個既定的事實(就是這樣,沒有為什麼),而不是分析後的結果。

要分析這類問題,我們需要知道一些更低抽象層級的知識。

幾乎所有主流前端框架的實現原理,都在踐行UI = f(state)這個公式,通俗的說 —— UI是對狀態的對映

這應該是前端狀態會出現的最低抽象層級了,所以我們從這個層級出發。

前端框架的實現原理

限於篇幅有限,這裡我們以最常見的ReactVue舉例。

在實現UI是對狀態的對映過程中,兩者的方向不同。

React並不關心狀態如何變化。每當呼叫更新狀態的方法(比如this.setState,或者useState dispatch...),就會對整個應用進行diff

所以在React中,傳遞給更新狀態的方法的,是狀態的快照,換言之,是個不可變的資料

Vue關心狀態如何變化。每當更新狀態時,都會對與狀態關聯的元件進行diff

所以在Vue中,是直接改變狀態的值。換言之,狀態是個可變的資料

這種底層實現的區別在單獨使用框架時不會有很大區別,但是會影響上層庫的實現(比如狀態管理庫)。

現在我們知道,透過前端框架,我們可以將狀態對映到UI。那麼如何管理好對應的對映關係呢?

換言之,如何將狀態與和他相關的UI約束在一起?

我們再往更高一級抽象看。

如何封裝元件

前端開發普遍採用元件作為狀態與UI的鬆散耦合單元

到這裡我們可以發現,如果僅僅會使用前端框架,那麼只能將元件看作是前端框架中既定的設計

但如果從更低一層抽象(前端框架的實現原理)出發,就能發現 —— 元件是為了解決框架實現原理中UI到狀態的對映的途徑。

那麼元件該如何實現,他的載體是什麼呢?從軟體工程的角度出發,有兩個方向可以探索:

  • 物件導向程式設計
  • 函數語言程式設計

物件導向程式設計的特點包括:

  • 繼承
  • 封裝
  • 多型

其中封裝這一特點使得物件導向程式設計很自然成為元件的首選實現方式,畢竟元件的本質就是將狀態與UI封裝在一起的鬆散耦合單元

ReactClassComponentVueOptions API都是類似實現。

但畢竟元件的本質是狀態與UI的鬆散耦合單元,在考慮複用性時,不僅要考慮邏輯的複用(邏輯是指操作狀態的業務程式碼),還要考慮UI的複用。所以物件導向程式設計的另兩個特性並不適用於元件。

框架們根據自身特點,在類物件導向程式設計的元件實現上,擴充了複用性:

  • React透過HOCrenderProps
  • Vue2透過mixin

經過長期實踐,框架們逐漸發現 —— 類物件導向程式設計的元件實現封裝帶來的好處不足以抵消複用性上的劣勢。

於是React引入了Hooks,以函式作為元件封裝的載體,借用函數語言程式設計的理念提高複用性。類似的還有Vue3中的Composition API

不管是ClassComponent還是FunctionComponentOptions 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
  • 不可變型別狀態將狀態與邏輯抽離到userModeluserReducer
  • 最終暴露給UI的都僅僅是changeName方法

當業務進一步複雜,Model本身需要更完善的架構,此時又是更高一級的抽象。

到這一層時已經脫離前端框架的範疇,上升到純狀態的管理,比如為mobx帶來結構化資料的mobx-state-tree

此時框架實現原理對Model的影響已經在更高的抽象中被抹去了,比如Redux-toolkitReact技術棧的解決方案,VuexVue技術棧的解決方案,但他們在使用方式上是類似的。

這是因為ReduxVuex的理念都借鑑自Flux,即使ReactVue在實現原理上有區別,但這些區別都被狀態管理方案抹平了。

更高的抽象

在此之上,對於狀態還有沒有更高的抽象呢?答案是肯定的。

對於常規的狀態管理方案,根據用途不同,可以劃分出更多細分領域,比如:

  • 對於表單狀態,收斂到表單狀態管理庫中
  • 對於服務端快取,收斂到服務端狀態管理庫中(React QuerySWR
  • 用完整的框架收斂前後端Model,比如RemixNext.js

總結

回到我們開篇提到的問題:

  • 為什麼專案中用Redux而不用Mobx
  • 為什麼要用Hooks而不用ClassComponent

現在我們已經能清晰的知道這兩個問題的相同點與不同點:

  • 相同點:都與狀態相關
  • 不同點:屬於不同抽象層級的狀態相關問題

要回答這些問題需要哪些知識呢?只需要知道問題涉及的狀態的抽象層級,以及比該層級更低的抽象層級對應的知識即可。

比如回答:為什麼專案中用Redux而不用Mobx

考慮當前抽象層級

ReduxMobx都屬於Model的實現,前者帶來一套類Flux的狀態管理理念,後者為React帶來響應式更新能力,在設計Model時我的專案更適合哪種型別?

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

考慮低一級抽象層級

專案用的ClassComponent還是FunctionComponentReduxMobx與他們結合使用時哪個組合更能協調好UI與邏輯的鬆散耦合?

考慮再低一級抽象層級

React的實現原理決定了他原生與不可變型別狀態更親和。Redux更契合不可變資料Mobx更契合可變資料。我的專案需要考慮這些差異麼?

當了解不同抽象層級需要考慮的問題後,任何寬泛的、狀態相關問題都能轉化成具體的、多抽象層級問題。

從不同抽象層級出發思考,就能更全面的回答問題。

相關文章