React Hooks 深入不淺出
這個標題可能不太好,但此文章確實不是一篇使用教程,而且也不會覆蓋太多點,建議時間充裕的還是應該完整地看下官網文件。
React Hooks 對於部分人來說可能還是陌生的,但還是阻止不了它成為了當前 React 社群裡「最」熱門的一個詞彙。

一開始瞭解到這個還是Dan Abramov 在十月底的時候發了一個推,是一篇文章 ofollow,noindex">Making Sense of React Hooks ,建議沒看過的先看下。看完第一感受就是:React 本就應該是這樣的啊!
看完這篇文章,希望你可以從整體上對 Hooks 有個認識,並對其設計哲學有一些理解,希望看的過程不要急,跟著我的思路走。
如果你想自己跟著文章一起練手,需要把 react
和 react-dom
更新到 16.7.0-alpha
及以上,如果配置了 ESLint,記得新增對應的Plugin。
插曲
長期以來很多人會把 Stateless Component
和 Functional Component
混為一談,我會試著跟他們解釋這不是一回事(不是一個維度),但當時 Functional Component
裡確實無法使用 state,我無論怎麼解釋都會顯得很無力。難道這冥冥之中都預示著會有類似 React Hooks 的東西出現?
React Hooks 的本質
稍微複雜點的專案肯定是充斥著大量的 React 生命週期函式(注意,即使你使用了狀態管理庫也避免不了這個),每個生命週期裡幾乎都承擔著某個業務邏輯的一部分,或者說某個業務邏輯是分散在各個生命週期裡的。

而 Hooks 的出現本質是把這種 面向生命週期程式設計 變成了 面向業務邏輯程式設計 ,你不用再去關心本不該關心的生命週期。

一個 Hooks 演變
我們先假想一個常見的需求,一個 Modal 裡需要展示一些資訊,這些資訊需要通過 API 獲取且跟 Modal 強業務相關,要求我們:
- 因為業務簡單,沒有引入額外狀態管理庫
- 因為業務強相關,並不想把資料跟元件分開放
- API 資料會隨機變動,因此需要每次開啟 Modal 才獲取最新資料
- 為了後期優化,不可以有額外的元件建立和銷燬
我們可能的實現如下:
class RandomUserModal extends React.Component { constructor(props) { super(props); this.state = { user: {}, loading: false, }; this.fetchData = this.fetchData.bind(this); } componentDidMount() { if (this.props.visible) { this.fetchData(); } } componentDidUpdate(prevProps) { if (!prevProps.visible && this.props.visible) { this.fetchData(); } } fetchData() { this.setState({ loading: true }); fetch('https://randomuser.me/api/') .then(res => res.json()) .then(json => this.setState({ user: json.results[0], loading: false, })); } render() { const user = this.state.user; return ( <ReactModal isOpen={this.props.visible} > <button onClick={this.props.handleCloseModal}>Close Modal</button> {this.state.loading ? <div>loading...</div> : <ul> <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li> <li>Gender: {user.gender}</li> <li>Phone: {user.phone}</li> </ul> } </ReactModal> ) } } 複製程式碼
我們抽象了一個包含業務邏輯的 RandomUserModal
,該 Modal 的展示與否由父元件控制,因此會傳入引數 visible
和 handleCloseModal
(用於 Modal 關閉自己)。
為了實現在 Modal 開啟的時候才進行資料獲取,我們需要同時在 componentDidMount
和 componentDidUpdate
兩個生命週期裡實現資料獲取的邏輯,而且 constructor
裡的一些初始化操作也少不了。
其實我們的要求很簡單:在合適的時候通過 API 獲取新的資訊,這就是我們抽象出來的一個 業務邏輯 ,為了這個業務邏輯能在 React 里正確工作,我們需要將其 按照 React 元件生命週期進行拆解 。這種拆解除了 程式碼冗餘 ,還 很難複用 。
下面我們看看採用 Hooks 改造後會是什麼樣:
QMLNX" rel="nofollow,noindex">完整演示地址
function RandomUserModal(props) { const [user, setUser] = React.useState({}); const [loading, setLoading] = React.useState(false); React.useEffect(() => { if (!props.visible) return; setLoading(true); fetch('https://randomuser.me/api/').then(res => res.json()).then(json => { setUser(json.results[0]); setLoading(false); }); }, [props.visible]); return ( // View 部分幾乎與上面相同 ); } 複製程式碼
很明顯地可以看到我們把 Class 形式變成了 Function 形式,使用了兩個 State Hook 進行資料管理(類比 constructor
),之前 cDM
和 cDU
兩個生命週期裡乾的事我們直接在一個 Effect Hook 裡做了(如果有讀取或修改 DOM 的需求可以看這裡)。做了這些,最大的優勢是 程式碼精簡 ,業務邏輯變的緊湊,程式碼行數也從 50+ 行減少到 30+ 行。
Hooks 的強大之處還不僅僅是這個,最重要的是這些業務邏輯可以隨意地的的抽離出去,跟普通的函式沒什麼區別(僅僅是看起來沒區別),於是就變成了可以 複用 的自定義 Hook。具體可以看下面的進一步改造:
// 自定義 Hook function useFetchUser(visible) { const [user, setUser] = React.useState({}); const [loading, setLoading] = React.useState(false); React.useEffect(() => { if (!visible) return; setLoading(true); fetch('https://randomuser.me/api/').then(res => res.json()).then(json => { setUser(json.results[0]); setLoading(false); }); }, [visible]); return { user, loading }; } function RandomUserModal(props) { const { user, loading } = useFetchUser(props.visible); return ( // 與上面相同 ); } 複製程式碼
這裡的 useFetchUser
為自定義 Hook,它的地位跟自帶的 useState
等比也沒什麼區別,你可以在其它元件裡使用,甚至在這個元件裡使用兩次,它們會天然地隔離開。
業務邏輯複用
這裡說的業務邏輯複用主要是需要跨生命週期的業務邏輯。單單按照元件堆積的形式組織程式碼雖然也可以達到各種複用的目的,但是會導致元件非常複雜,資料流也會很亂。元件堆積適合 UI 佈局,但是不適合邏輯組織。為了解決這些問題,在 React 發展過程中,產生了很多解決方案,我認知裡常見的有以下幾種:
Mixins
壞處遠遠大於帶來的好處,因為現在已經不再支援,不多說,可以看看這篇文章: Mixins Considered Harmful 。
Class Inheritance
官方 很不推薦此做法,實際上我也沒真的看到有人這麼做。
High-Order Components (HOC)
React 高階元件 在封裝業務元件上簡直是屢試不爽,它的實現是把自己作為一個函式,接受一個元件,再返回一個元件,這樣它可以統一處理掉一些業務邏輯並達到複用目的。
比較常見的一個就是 react-redux
裡的 connect
函式:

(圖片來自這裡)
但是它也被很多人吐槽巢狀問題:

(圖片來自這裡)
Render Props
Render Props 其實很常見,比如React Context API:
class App extends React.Component { render() { return ( <ThemeProvider> <ThemeContext.Consumer> {val => <div>{val}</div>} </ThemeContext.Consumer> </ThemeProvider> ) } } 複製程式碼
它的實現思路很簡單,把原來該放「元件」的地方,換成了回撥,這樣當前元件裡就可以拿到子元件的狀態並使用。
但是,同樣這會產生 Wrapper Hell 問題:

(圖片來自這裡)
Hooks
Hooks 本質上面說了,是把 面向生命週期程式設計 變成了 面向業務邏輯程式設計 ,寫法上帶來的優化只是順帶的。
這裡,做一個類比, await/async
本質是把 JS 裡非同步程式設計思維變成了同步思維,寫法上表現出來的特點就是原來的 Callback Hell 被打平了。
總結對比:
await/async
這裡不得不客觀地說,HOC 和 Render Props 還是有存在的必要,一方面是支援 React Class,另一方面,它們不光適用於純邏輯封裝,很多時候也適合邏輯 + 元件的封裝場景,雖然此時使用 Hooks 也可以,但是會顯得囉嗦點。另外,上面詬病的最大的問題 Wrapper Hell,我個人覺得使用Fragment 也可以基本解決。
狀態盒子
首先,React Hooks 的設計是反直覺的,為什麼這樣說呢?可以先試著問自己:為什麼 Hooks 只能在其它 Hooks 的函式或者 React Function 元件裡?
在我們的認知裡,React 社群一直推崇函式式、純函式等思想,引入 Hooks 概念後的 Functional Component
變的不再純了, useXxx
與其說是一條執行語句,不如說是一個 宣告 。宣告這裡放了一個「狀態盒子」,盒子有輸入和輸出,剩下的內部實現就一無所知,重要的是,盒子是有 記憶 的,下次執行到此位置時,它有之前上下文資訊。
類比「程式碼」和「程式」的區別,前者是死的,後者是活的。表示式 c = a + b
表示把 a
和 b
累加後的值賦值給 c
,但是如果寫成 c := a + b
就表示 c
的值由 a
和 b
相加得到。看起來表述差不多,但實際上,後者隱藏著一個時間的維度,它表示的是一種 聯絡 ,而不單單是個運算。這在RxJS 等庫中被大量使用。

這種宣告目前是通過很弱的 use
字首標識的(但是設計上會簡潔很多),為了不弄錯每個盒子和狀態的對應關係,書寫的時候 Hooks 需要 use
開頭且放在頂層作用域,即不可以包裹 if/switch/when/try
等。如果你按文章開頭引入了那個 ESLint Plugin 就不用擔心會弄錯了。