Hook(鉤子)是React v16.8新引入的特性,能以鉤子的形式為函數組件附加類組件的狀態、生命周期等特性。React的類組件有難以拆分、測試,狀態邏輯分散,難以復用等問題,雖然可以通過渲染屬性(Render Props)和高階組件來提取狀態邏輯,但會形成層層嵌套,而使用Hook后的函數組件就能避免這些問題。
  Hook本質上是一種特殊的JavaScript函數,名稱以use為前綴,在使用它時需要遵循兩條規則,如下所列:
  (1)在循環、條件語句或嵌套函數中調用Hook是不允許的,必須在函數的最頂層調用,確保Hook的調用順序。
  (2)只能在React的函數組件或自定義的Hook中調用Hook。
  這兩條規則可以結合后文的分析慢慢體會,接下來會詳細講解幾個內置的Hook,并且會介紹如何自定義Hook,文中的示例來源于[官網](https://react.docschina.org/docs/hooks-intro.html)。
## 一、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索引](https://react.docschina.org/docs/hooks-reference.html)。
**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。
*****
> 原文出處:
[博客園-React躬行記](https://www.cnblogs.com/strick/category/1455720.html)
[知乎專欄-React躬行記](https://zhuanlan.zhihu.com/pwreact)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020