「 思考 」 React Hooks 的設計哲學

皮小蛋發表於2020-07-20

React Hooks Complete Guide | useState() | useEffect() | Rules for React  Hooks

背景

React Hooks 已經出來有段時間了, 很多小夥伴或多或少都用過。

今天呢,我們就回頭再看一下這個東西,思考一下,這個東西為什麼會出現,它解決了什麼問題, 以及背後的設計理念。

正文

如果你有 Hooks 的使用經驗, 可以思考一下這兩個問題:

  1. Hooks 為什麼會產生
  2. Hooks 解決了什麼問題

我們先主要圍繞這兩點展開討論。

1. Hooks 為什麼會產生

在正式開始這個話題前, 我們先回顧一下react的發家史.

2013年5月13號, 在JS Conf 上釋出了第一個版本0.3.0.

我第一次接觸React 是在2015年, 對createClass語法記憶猶新:

image.png

createClass 是當時第一種用於建立 React 元件的語法, 因為當時Javascript 還沒有成形的 Class 體系。

這種情況在2015年1月17號得到了改變。

這時候, ES6 正式釋出,支援 Class 語法。

這時候面臨一個選擇:

繼續用自家的 createClass 呢 還是使用新的 ES6 Class

畢竟 createClass 又不是不能用, 而且用著還挺順手。

最後, React 還是選擇了擁抱新趨勢, 使用ES6 Class。

並在 React 0.13.1 Beta1版本, 開始支援使用原生Javasciprt Class語法。 我找到了如下說明:

image.png

大意就是: 我們並不想自己單獨搞一套, 大家習慣怎麼用, 我們就怎麼搞。

基於這個變化, React Class 元件就變成了我們之前經常見到的這樣:

image.png

是不是很熟悉。

生命週期方法和之前保持一致,變化的是元件初始化的部分。

從原本的getInitinalState 變成了constructor

經歷過這個階段的小夥伴肯定對以下程式碼段非常熟悉:

constructor(props) {
  super(props); // ???
  
  this.state = {};
  
  this.xxx = this.xxx.bind(this); // ???
}

在元件初始化的時候, 必須手動super(props) 一下, 至於為什麼這麼做, 本文不做討論, 有興趣的可以看一下這篇譯文: 為什麼要寫Super(props)

Class Fields 也允許我們跳過這一步:

class xxx extends React.Component { 
  state = {};
}

到這一步, 已經解決了兩個令人難受的點:

  1. super
  2. bind

已經足夠OK了, 是吧。

其實還不夠。

我們在編寫react 應用的時候, 難以避免的一件事就是: 拆分react 元件。

把一個複雜的UI檢視拆分成不同的模組, 然後組合在一起。

這也是 react 本身推崇的理念: 萬物皆可是元件。

這種設計很棒棒, 但依舊有很多問題。

我認為主要是亮點:

  1. 元件內邏輯的割裂
  2. 邏輯複用困難

1. 先說 邏輯上的割裂

基於生命週期的設計, 使得我們經常寫出邏輯割裂的程式碼:

image.png

同樣的邏輯, 我們需要在不同的生命週期中去實現。

在一個大型的app 中, 類似的邏輯會有很多, 摻雜在一起。

後人要去修改的時候, 不得不使用上下左右反覆橫跳之術, 令人十分痛苦。

2. 邏輯複用困難

我們都知道, react 應用其實是由一些列 UI 套件組合而成的, 這些套件有些有狀態, 有些沒有狀態。

image.png

把這些元件組合在一起,處理好複用, 其實是有一定困難的。

比如,假設在另外一個元件,有和上圖相似的邏輯, 怎麼辦呢?

Copy & Paste 顯然是可以的, 但卻不是最優雅的。

React 本身並不提供解決方案,但是機智的網友們逐漸摸索出了一些改善這個問題的方法:

  1. High Order Components
  2. Render Props

High Order Components 為例, 看一下最簡的例子

為元件都加入一個data屬性, 然後返回這個增強的元件:

image.png

邏輯並不複雜。

回到我們最初的那個例子, 現在要把這部分邏輯抽離出來, 實現一個WithRepos 高階方法:

image.png

使用的時候, 包裹一下就可以了:

image.png

Render Props 也是同樣的目的, 不作贅述, 可參考:Render Props

這兩種做法, 都可以改善邏輯複用的困境,但同時又引入了新的問題。

還是以高階元件為例, 比如我們對一個元件要加入多個增強功能,顯而易見, 程式碼就變成了:

export default withA(
  withB (
     withC (
        withD (
           Component
        )
     )
  )
)

Render Props 也一樣, 這兩種模式都會限制你的元件結構,隨著功能的增加, 包裹的層數越來越多,陷入所謂的 wrapper hell之中。

這種情況並不是我們想要的。

寫到這裡, 我們進行一個簡單的總結, 整理一下遇到的問題:

  1. 我們使用 Class 語法來生成元件,super語法很煩, 但是可以跳過。
  2. this 讓人懵逼。
  3. 基於生命週期的設計, 容易造成邏輯上的割裂, 不容易維護。
  4. React 沒有以後好的模式來解決邏輯複用問題。

所以, 迫切需要一種新的模式來解決以上這些問題。

理想中, 這種模式要具備以下特點:

  1. 簡單好用
  2. 足夠靈活
  3. 方便組合
  4. 擴充套件性強

那麼, 這種新的模式該如何設計呢?

此處引用一下John Carmack的話:

image.png

而且, Javascript 本身對 function 也是天生友好。

所以, 這時候要做的就是:

  • 拋棄 React.Component
  • 擁抱 function

image.png

在這個背景下, Hooks 應運而生。

2. Hooks 解決了什麼問題

擁抱 Function, 面前就有三座大山需要解決:

  1. 元件 State
  2. 生命週期
  3. 邏輯複用難題

2.1 State

image.png

State Hook 的標準形式是返回一個元組, 包含兩個元素:

image.png

使用起來也非常的簡單:

image.png

至此,有了state hook, function 就具備了基礎的狀態管理能力:

  1. 元件 State ✅
  2. 生命週期
  3. 邏輯複用難題

2.2 Lifecyles

這一步, 我們先忘記傳統 Class Component 的生命週期方法, 想一下, 如何在 Function 中實現類似的能力。

我們要用這樣的能力去實現,比如:

  1. 狀態變更
  2. 資料獲取
  3. 等等

基於這樣的思考, useEffect 問世了。

useEffect 賦予了Function 在元件內部執行副作用的能力。

image.png

就形式而言, Effect Hook 接受兩個引數:

  1. 一個 function.
  2. 一個可選的 array.

簡單的例子:

image.png

當 username 變化時, 就修改document title.

⚠️ 注意

有時候,你也許會不經意間把 Effect 寫成一個 async 函式:

強烈建議你不要這樣做。

useEffect 約定:

Effect 函式要麼沒有返回值要麼返回一個 Cleanup 函式

而這裡 async 函式會隱式地返回一個 Promise,直接違反了這一約定,會造成不可預測的結果

至此, Function 元件也有了應該具備的生命週期方法。

  1. 元件 State ✅
  2. 生命週期 ✅
  3. 邏輯複用難題

只剩最後一個課題: 邏輯複用。

2.3 Sharing Non-Visual Logic

傳統而言, 我們把頁面拆分成一個個UI元件, 然後把這個UI元件組合起來。 這種情況最終也不可避免的誕生了 HOC & Render Props 等模式來改善邏輯複用問題。

你可能會想, React Hooks 可能會有新的解決辦法。

辦法的確是有, 它就是Custom Hooks.

你可以把需要複用的邏輯抽成一個個單獨的Custom Hook, 在需要用的地方使用。

舉個例子:

把需要複用的邏輯抽離:

image.png

在需要用到的地方使用:

image.png

這樣, 我們就輕鬆而又自然的實現了邏輯的複用。

  1. 元件 State ✅
  2. 生命週期 ✅
  3. 邏輯複用難題 ✅

至此, 三個難題得以解決。

Hooks 的價值所在

回頭我們再看這個問題, 其實從始至終, 要解決的問題只有一個:

提升程式碼複用以及組合的能力。

順帶的, 也一定程度上提升了元件的內聚性, 減少了維護成本:

相關的邏輯都在單獨的一塊, 改需求的時候,不用需要施展上下左右反覆橫跳之術,提早了下班時間, 多好。

結尾

知其然,也要知其所以然。

我們在自己平時的搬磚運動中, 也要考慮自己的程式碼是否具備一下能力:

  1. 簡單好用
  2. 足夠靈活
  3. 方便組合
  4. 擴充套件性強

不坑自己, 也不坑別人, 早點下班。

好了, 別的就不扯了, 希望這篇文章能給你一些啟發和思考。

才疏學淺, 文章若有錯誤, 歡迎留言之正。

相關文章