1. 程式人生 > >React躬行記(15)——React Hooks

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。