React躬行記(15)——React Hooks
Hook(鉤子)是React v16.8新引入的特性,能以鉤子的形式為函式元件附加類元件的狀態、生命週期等特性。React的類元件有難以拆分、測試,狀態邏輯分散,難以複用等問題,雖然可以通過渲染屬性(Render Props)和高階元件來提取狀態邏輯,但會形成層層巢狀,而使用Hook後的函式元件就能避免這些問題。
Hook本質上是一種特殊的JavaScript函式,名稱以use為字首,在使用它時需要遵循兩條規則,如下所列:
(1)在迴圈、條件語句或巢狀函式中呼叫Hook是不允許的,必須在函式的最頂層呼叫,確保Hook的呼叫順序。
(2)只能在React的函式元件或自定義的Hook中呼叫Hook。
這兩條規則可以結合後文的分析慢慢體會,接下來會詳細講解幾個內建的Hook,並且會介紹如何自定義Hook,文中的示例來源於官網。
一、State Hook(狀態鉤子)
先來看一個簡單的類元件,Btn元件會渲染出一個按鈕,每次點選按鈕,其文字會加一。
import React from "react"; class Btn extends React.Component { constructor() { super(); this.state = { count: 0 }; this.dot = this.dot.bind(this); } dot() { this.setState({ count: this.state.count + 1 }) } render() { return <button onClick={this.dot}>{this.state.count}</button>; } }
然後將Btn元件改成相同功能的函式形式,如下程式碼所示,沒有了建構函式和render()方法,通過useState()為函式元件附加狀態。
import { useState } from "react"; function Btn() { const [count, setCount] = useState(0); return (<button onClick={() => setCount(count + 1)}>{count}</button>); }
useState()是一個鉤子函式,它的引數是狀態的初始值,返回一個數組,包含兩個元素:當前狀態和更新狀態的函式。通過陣列解構的方式聲明瞭一個名為count的狀態變數和一個名為setCount的函式,相當於類元件中的this.state.count和this.setState()。在點選事件中讀取狀態或呼叫更新狀態的函式都不需要this。
注意,useState()可以被多次呼叫,React會根據useState()的出現順序保證狀態的獨立性,並且與this.setState()不同的是,更新狀態是替換而不是合併。
二、Effect Hook(副作用鉤子)
在React元件中有兩種常見的副作用:無需清除和需要清除,接下來會逐個講解。
1)無需清除
在React更新DOM之後會執行一些無需清除的副作用,例如向伺服器請求資料、變更DOM結構、記錄日誌等。在類元件中,這些副作用常在componentDidMount()和componentDidUpdate()生命週期方法中執行。以上一節的Btn元件為例,在更新計數後,修改頁面標題,如下所示(只列出了核心程式碼)。
class Btn extends React.Component { componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; } }
注意,兩個函式中的程式碼是重複的,因為很多情況下,在元件掛載和更新時會執行相同的操作,而React並未提供每次渲染之後可回撥的函式。
接下來用useEffect()鉤子函式實現相同功能,同樣只列出了核心程式碼,如下程式碼所示。useEffect()使得相同功能的副作用不用再分散到不同的生命週期中,即按照用途分離副作用。
import { useEffect } from "react"; function Btn() { useEffect(() => { document.title = `You clicked ${count} times`; }); }
useEffect()可接收兩個引數,第一個引數是回撥函式,叫做Effect,在每次渲染(包括第一次掛載和後續的DOM更新)之後Effect都會被執行,其中每次接收的Effect都是新的,不用擔心狀態過期的問題;第二個引數是可選的陣列(由Effect的依賴項組成),用於控制Effect的執行,而是否執行Effect將取決於陣列中的元素是否發生了變化,例如將count變數作為陣列的元素(如下程式碼所示),當count的值與重新渲染後的count的值一樣時,React會忽略這個Effect,優化效能。
useEffect(() => { document.title = `You clicked ${count} times`; }, [count]);
當把一個空陣列([])傳給useEffect()時,Effect只會執行一次,即僅在元件掛載和解除安裝時執行。由於Effect不依賴state或props中的任意值,因此永遠都不需要重複執行。
useEffect()相當於componentDidMount()、componentDidUpdate()和componentWillUnmount()三個生命週期方法的組合,但與componentDidMount()或componentDidUpdate()不同,使用useEffect()會非同步執行副作用,可避免阻塞瀏覽器更新檢視。
2)需要清除
有些副作用是必須清除的,例如訂閱的外部資料來源,將其清除後,可防止記憶體洩露。在類元件中,通常會在componentDidMount()中設定訂閱,並在componentWillUnmount()中執行清除。
假設有一個ChatAPI模組,用於訂閱好友的線上狀態,如下所示(只有關鍵部分),其中componentDidMount()和componentWillUnmount()處理的是關聯的副作用。
class FriendStatus extends React.Component { componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } }
接下來用函式元件實現相同的功能,同樣只有關鍵部分的程式碼。由於新增和移除訂閱的邏輯有很強的緊密性,因此useEffect()將它們組織在一起。當Effect返回一個函式時,React將在執行清除操作時呼叫它,如下所示。
function FriendStatus(props) { useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); }
注意,React會在執行當前Effect之前對上一個Effect進行清除,也就是說,副作用並不僅在元件解除安裝時被執行。
三、自定義Hook
自定義的Hook用於儲存元件中可複用的邏輯,它的引數和返回值都沒有特殊要求,類似於一個普通的函式,但為了遵循Hook的規則,其名稱必須以use開頭。接下來將之前的FriendStatus元件中訂閱好友線上狀態的邏輯抽離到自定義的useFriendStatus()中,其引數為friendID,返回值為好友當前的狀態,如下所示。
function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; }
在FriendStatus元件中呼叫自定義的Hook,其內部邏輯將變得非常簡潔,如下所示。
function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
四、其它Hook
除了上面所講解的兩個內建Hook,React還提供了其它功能的Hook,例如useContext()、useCallback()、useMemo()、useLayoutEffect()等,具體可參考官方的API索引。
1)useContext()
接收一個由React.createContext()建立的Context物件,返回該Context的當前值(即要傳送的資料)。呼叫了useContext()的元件會在Context值發生變化時重新渲染。
2)useCallback()
包含兩個引數,第一個是回撥函式,第二個是依賴項陣列,返回回撥函式的記憶版本。當某個依賴項發生改變時,會更新回撥函式。注意,依賴項陣列不會作為引數傳給回撥函式。
3)useMemo()
包含回撥函式和依賴項陣列兩個引數,回撥函式的返回值就是useMemo()的返回值,它會被快取,並且僅在某個依賴項發生改變時才重新計算它。之前的useCallback(fn, deps)相當於useMemo(() => fn, deps)。
4)useLayoutEffect()
函式簽名與useEffect()相同,但呼叫時機不同,它會在所有的DOM更新之後同步呼叫Effect,也就是在瀏覽器更新檢視之前呼叫Effect。