Hook 是 react 16.8 推出的新特性,具有如下優(yōu)點(diǎn):
- Hook 使你在無需修改組件結(jié)構(gòu)的情況下復(fù)用狀態(tài)邏輯?!远x hook
- Hook 將組件中相互關(guān)聯(lián)的部分拆分成更小的函數(shù)(比如設(shè)置訂閱或請求數(shù)據(jù))——Effect
- Hook 使你在非 class 的情況下可以使用更多的 React 特性?!獡肀Ш瘮?shù)式組件
1. 簡介
Hook 就是 JavaScript 函數(shù),但是使用它們會有兩個(gè)額外的規(guī)則:
- 只能在函數(shù)最外層調(diào)用 Hook,也即Hook 需要在我們組件的最頂層調(diào)用。不要在循環(huán)、條件判斷或者子函數(shù)中調(diào)用,這會破壞更新時(shí) hook 順序的一致性,造成數(shù)據(jù)讀取錯(cuò)誤。
- 只能在 React 的函數(shù)組件中調(diào)用 Hook。不要在其他 JavaScript 函數(shù)中調(diào)用。(還有一個(gè)地方可以調(diào)用 Hook —— 就是自定義的 Hook 中)
可以通過 ESLint 配置來提醒自己遵循 Hook 開發(fā)規(guī)則:
安裝插件
npm install eslint-plugin-react-hooks --save-dev配置
package.json中的eslint-config:
// 你的 ESLint 配置
"eslintConfig": {
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 檢查 Hook 的規(guī)則
"react-hooks/exhaustive-deps": "warn" // 檢查 effect 的依賴
}
}
2. React Hook 的基礎(chǔ) API (附實(shí)例)
具體 API 介紹可以查閱官網(wǎng) Hook API Reference
2.1 組件中的狀態(tài)——useState
在使用 class 定義組件時(shí),可以通過 this.state 定義組件內(nèi)的狀態(tài)屬性,在 render 時(shí)使用 this.state[key] 獲取狀態(tài)值,使用 this.setState 來修改狀態(tài)。Hook 中提供了 useState 方法,用于快速定義函數(shù)組件內(nèi)的狀態(tài)和對應(yīng)的更改函數(shù)。
使用方法:const [state, setState] = useState(defaultValue)
實(shí)例:
import React, { useState } from 'react'
function Counter() {
// 初始化 count = 0
const [count, setCount] = useState(0)
return (<div>
<p>你點(diǎn)擊了 {count} 次</p>
{/* 直接調(diào)用 setCount,傳入新的值,賦值給 count */}
<button onClick={()=> setCount(count+1)}>點(diǎn)擊</button>
</div>)
}
2.2 組件中的生命周期——useEffect
函數(shù)式組件在發(fā)生更新時(shí),都會順序執(zhí)行函數(shù)主體,相當(dāng)于類組件中的 render 函數(shù)。而在 render 過程中,是不允許執(zhí)行改變 DOM、添加訂閱、設(shè)置定時(shí)器、記錄日志等包含副作用的操作,因此在類組件中,我們通常在生命周期函數(shù)中執(zhí)行必要的包含副作用操作。在函數(shù)式組件中,useEffect 提供了執(zhí)行副作用操作的支持,當(dāng) React 渲染組件時(shí),會保存已使用的 effect,并在更新完 DOM 后執(zhí)行它。useEffect ≈ componentWillMount + componentDidUpdate + componentWillUnmount,其內(nèi)部可以訪問到組件的 props 和 state。
使用方法:useEffect( didUpdateFn )
實(shí)例:
import React, { useState, useEffect } from 'react'
function Counter() {
// 初始化 count = 0
const [count, setCount] = useState(0)
// 如果第二個(gè)參數(shù)為空,則只有在組件被銷毀時(shí)才解綁,也就是 副作用 等價(jià)于 componentDidMount,解綁 等價(jià)于 componentWillUnMount
useEffect(() => {
console.log('useEffect => 組件掛載')
// 返回一個(gè)清除函數(shù),當(dāng)副作用中有定時(shí)器或監(jiān)聽事件時(shí)清除
return () => {
console.log('useEffect => 組件被銷毀')
};
}, [])
// 第二個(gè)參數(shù),每次當(dāng) count 發(fā)生變化就執(zhí)行解綁原來的數(shù)據(jù)并重新執(zhí)行副作用
useEffect(() => {
console.log('useEffect => count 數(shù)據(jù)掛載')
return () => {
console.log('useEffect => count 數(shù)據(jù)解綁')
};
}, [count])
return (<div>
<p>你點(diǎn)擊了 {count} 次</p>
<button onClick={()=> setCount(count+1)}>點(diǎn)擊</button>
</div>)
}
React 會在調(diào)用一個(gè)新的 effect 之前對前一個(gè) effect 進(jìn)行清理。在上述程序中,當(dāng) count 值更新時(shí),會先輸出 ’useEffect => count 數(shù)據(jù)掛載‘,再輸出 ’useEffect => count 數(shù)據(jù)掛載‘。因此當(dāng)我們在 useEffect 中設(shè)置定時(shí)器/事件時(shí),通過返回一個(gè)清除函數(shù),使得在下一次依賴發(fā)生更新時(shí),能夠清除上一次的定時(shí)器/事件,以此避免內(nèi)存泄露。否則每次依賴更新時(shí),都會增加一個(gè)定時(shí)器/事件。
2.3 跨組件通信——useContext
在跨組件通信時(shí),可以借助 context 實(shí)現(xiàn)組件間的傳值。useContext 用于快速獲取組件上層最近的 contextObj.Provider 所提供的 value 值,等價(jià)于 contextObj.Consumer。
使用方法:useContext(ContextObj),contextObj 是 React.createContext 返回的 context 對象
實(shí)例:
import React, { useState, useEffect, useContext, createContext } from 'react'
const ColorContext = createContext()
function Container() {
const [color, setColor] = useState('#ffff00')
const toggleColor = () => {
const saturation = () => Math.random() * 255
setColor(`rgb(${saturation()}, ${saturation()}, ${saturation()})`)
}
// 1. 使用 ColorContext.Provider 將 color 值傳給內(nèi)部的組件
// 3. 當(dāng)按鈕點(diǎn)擊切換背景色時(shí), color 值發(fā)生變化,將通知到 Counter 組件中
return (<ColorContext.Provider value={color}>
<Counter />
<button onClick={toggleColor}>切換背景色</button>
</ColorContext.Provider>)
}
function Counter() {
// ...
// 2. 使用 useContext 返回最近的 Provider 提供的 value 值,本例中也即 color 值,并訂閱 color 值的變化
const color = useContext(ColorContext)
return (<div>
<p style={{backgroundColor: color}}>你點(diǎn)擊了 {count} 次</p>
<button onClick={()=> setCount(count+1)}>點(diǎn)擊</button>
</div>)
}
2.4 復(fù)雜狀態(tài)管理——useReducer
在 redux 狀態(tài)管理中,使用 reducer 根據(jù) action 的不同,對 state 執(zhí)行不同的操作。在 hook 中,useState 支持直接修改 state,但是當(dāng)修改邏輯較為復(fù)雜時(shí),可以改用 useReducer 來定義不同的更改行為。通過傳入一個(gè)形如 (state, action) => {} 的 reducer,返回狀態(tài)及其 dispatch 函數(shù)。還可以使用后面的兩個(gè)參數(shù)對 state 執(zhí)行初始化操作,initialArg 將作為 init 函數(shù)的參數(shù)傳入。
使用方法:const [state, dispatch] = useReducer(reducer, initialArg, init)
實(shí)例:
import React, { useState, useEffect, useContext, createContext, useReducer } from 'react'
function Container() {
// ...
return (<ColorContext.Provider value={color}>
<Counter initialCount={0}/>
<button onClick={toggleColor}>切換背景色</button>
</ColorContext.Provider>)
}
function Counter() {
// ...
const init = initialCount => ({count: initialCount})
const [state, dispatch] = useReducer((state, action) => {
switch(action.type) {
case 'add':
return {count: state.count + 1}
case 'sub':
return {count: state.count - 1}
case 'reset':
return init(action.payload)
default:
return state
}
}, initialCount, init)
return (<div>
<p style={{backgroundColor: color}}>你點(diǎn)擊了 {state.count} 次</p>
<button onClick={() => dispatch({type: 'add'})}>+</button>
<button onClick={() => dispatch({type: 'sub'})}>-</button>
<button onClick={() => dispatch({type: 'reset', payload: initialCount})}>Reset</button>
</div>)
}
2.5 組件性能優(yōu)化——useCallback / useMemo
在組件生命周期的應(yīng)用中,常常有利用 shouldCompnentUpdate 判斷參數(shù)/狀態(tài)的相等性,避免不必要的組件渲染。 useCallback 也是一種類似的組件優(yōu)化手段,其返回一個(gè) memoized 函數(shù),僅在依賴項(xiàng)發(fā)生變化時(shí),函數(shù)體才會更新。useCallback(fn, deps) 相當(dāng)于 useMemo(() => fn, deps),不同的是 useMemo 返回的是一個(gè) memoized 值,當(dāng)依賴項(xiàng)發(fā)生變化時(shí),fn 才會執(zhí)行,該值才會發(fā)生更新。傳入 useMemo 的函數(shù)會在渲染期間執(zhí)行,因此在 useMemo 內(nèi)部,不要執(zhí)行與渲染無關(guān)的操作。依賴項(xiàng)并不會作為參數(shù)傳入回調(diào)函數(shù)中,但內(nèi)部執(zhí)行函數(shù)可以直接使用依賴項(xiàng),如 fn(deps) 。
使用方法:useCallback(() => { doSomething() }, depsArr) / useMemo(() => doSomething(), depsArr)
實(shí)例:
import React, { useState, useEffect, useContext, createContext, useReducer, useMemo } from 'react'
// ...
function Counter() {
// ...
const [asyncName, setName] = useState('')
function sayName(name) {
setTimeout(() => {
// 模擬異步請求,并根據(jù)請求結(jié)果設(shè)置狀態(tài)值
console.log(`${name} 正在操作`)
setName(`user_${name}`)
}, 1000)
}
// 直接調(diào)用 sayName 的話,每次 state.count 發(fā)生變化時(shí),雖然 asyncName 并不會變化,但 sayName 每次都會被執(zhí)行。如果是一個(gè)比較耗時(shí)的異步請求,將降低組件的性能
// sayName(name)
// 對比直接調(diào)用,使用 useMemo,能夠避免 count 變化時(shí) sayName 的頻繁調(diào)用,從而優(yōu)化組件性能
// 僅在依賴項(xiàng) name 值發(fā)生變化時(shí),sayName 方法才會被執(zhí)行
useMemo(() => sayName(name), [name])
return (<div>
<h1>用戶名:{asyncName}</h1>
{/* ... */}
</div>)
}
效果:
- 直接調(diào)用
sayName

- 使用
useMemo
僅有更新 name 時(shí)才會打印 xxx 正在操作
2.6 組件內(nèi)值的保存——useRef
ref 是一種訪問 DOM 的方式,useRef 返回一個(gè)“盒子”,可以在其 current 屬性中保存一個(gè)任何類型的可變值,如 DOM 元素、定時(shí)器、訂閱器等。useRef 在每次渲染時(shí)返回同一個(gè) ref 對象,當(dāng) ref 對象內(nèi)容發(fā)生變化時(shí),useRef 并不會通知你。變更 .current 屬性不會引發(fā)組件重新渲染。
使用方法:const oRef = useRef(initialValue)
實(shí)例:
import React, {useRef, useEffect} from 'react'
function InputItem() {
const inputEle = useRef(null)
const timerId = useRef(null)
const [time, setTime] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// 使用函數(shù)更新的方式,避免依賴項(xiàng)
setTime(t => t + 1)
}, 1000)
timerId.current = id
return () => clearInterval(id)
}, [])
const focusBtnClick = () => {
// inputEle.current 已經(jīng)掛載到 DOM 中的文本輸入框元素上
inputEle.current.focus()
}
const clearBtnClick = () => {
// timerId.current 已經(jīng)被寫入了定時(shí)器的 id,可以在 click 事件中中止定時(shí)器
clearInterval(timerId.current)
}
return (<div>
<h2>定時(shí)器數(shù)值為:{time}</h2>
<input type='text' ref={inputEle} />
<button onClick={focusBtnClick}>focus</button>
<button onClick={clearBtnClick}>stop</button>
</div>)
}
效果:

2.7 其它
以下三個(gè) hook 都是極少使用的方法,簡單介紹其應(yīng)用,有必要時(shí)可以查閱官方文檔
-
useImperativeHandle:用于自定義暴露給父組件的子組件內(nèi)部某一 ref 實(shí)例值,與forwardRef(將內(nèi)部某一 ref 實(shí)例值全部暴露給父組件) 配合使用。 -
useLayoutEffect:作用同useEffect,不同的是useEffect是在 DOM 元素渲染完成后執(zhí)行,而useLayoutEffect是與 DOM 更新同步執(zhí)行。 -
useDebugValue:用于在 React 開發(fā)者工具中顯示自定義 hook 的標(biāo)簽。
3. 自定義 Hook
在函數(shù)化開發(fā)時(shí),我們常常將多個(gè)函數(shù)間共用的邏輯抽離為某一功能函數(shù),增強(qiáng)代碼的復(fù)用性。而在組件化開發(fā)過程中,兩個(gè)組件之間也可能存在同樣的功能邏輯,比如需求列表和詳情頁都需要獲取需求項(xiàng)的狀態(tài)(規(guī)劃中/進(jìn)行中/已完成),此時(shí)可以把 “查詢需求項(xiàng)狀態(tài)” 這一功能用自定義 hook 抽離出來,不僅能夠提高代碼復(fù)用性和可讀性,還能方便測試。自定義 hook 命名需要以 use 開頭,以方便 react 自動(dòng)檢查是否違反了 hook 規(guī)則。目前,也有很多第三方 hook 實(shí)現(xiàn):https://github.com/streamich/react-use
實(shí)例:
import React, { useState, useEffect } from 'react'
// 自定義 hook : 根據(jù)需求項(xiàng)的 id 值查詢狀態(tài)
function useStatus(demandId) {
// 需求項(xiàng)狀態(tài),0 - 規(guī)劃中, 1 - 進(jìn)行中, 2 - 已完成
const [status, setStatus] = useState(0)
useEffect(() => {
// 模擬一下異步請求
const getStatus = setTimeout(() => {
setStatus(demandId % 3)
}, 200)
return () => {
clearTimeout(getStatus)
}
}, [demandId])
const description = ['規(guī)劃中', '進(jìn)行中', '已完成']
return {code: status, status: description[status]}
}
// 需求列表組件
function DemandList() {
const [list, setList] = useState([])
const [selectedId, setSelectedId] = useState(null)
useEffect(() => {
// 模擬一下數(shù)據(jù)
let mockList = []
for(let i = 0; i <= 9; i++) {
const id = i + Math.round(Math.random() * 100)
mockList.push({
id,
name: `需求${id}`
})
}
setList(mockList)
}, [])
return (<div>
<ul>
{list.map( demand => (
<DemandItem
demandId = {demand.id}
selectedMethod = {setSelectedId}
>
{demand.name}
</DemandItem>
) )}
</ul>
<hr />
{/* 這里為了偷懶,就沒有用路由,而是直接顯示在下面 */}
{ selectedId && <DemandDetail demandId = {selectedId} />}
</div>)
}
// 需求項(xiàng)組件
function DemandItem({demandId, selectedMethod, children}) {
// 調(diào)用自定義 hook 獲取需求項(xiàng)狀態(tài)
const {code, status} = useStatus(demandId)
const color = ['#F4A460', '#FFD700', '#32CD32']
return (<li>
<span style = {{display: 'inline-block', width: '100px'}}>{children}</span>
<span
style = {{backgroundColor: color[code], cursor: 'pointer'}}
onClick = {() => {selectedMethod(demandId)}}
>{status}</span>
</li>)
}
// 需求詳情頁組件
function DemandDetail({demandId}) {
// 調(diào)用自定義 hook 獲取需求項(xiàng)狀態(tài)
const {status} = useStatus(demandId)
const [info, setInfo] = useState({})
useEffect(() => {
// 根據(jù) id 查詢需求的詳細(xì)信息
setInfo({
name: `需求${demandId}`,
detail: '這是需求詳情信息呀~這是需求詳情信息呀~這是需求詳情信息呀~這是需求詳情信息呀~這是需求詳情信息呀~'
})
}, [demandId])
return (<div>
<h3>項(xiàng)目名稱為:{info.name}({status})</h3>
<p>{info.detail}</p>
</div>)
}
export default DemandList
效果:

