你不知道的 React Hooks(萬字長文,快速入門必備)
什么是 Hooks
Hook 是 React 16.8 的新增特性。
Hooks 本質上就是一類特殊的函數(shù),它們可以為你的函數(shù)型組件(function component)注入一些特殊的功能,讓您在不編寫類的情況下使用 state(狀態(tài)) 和其他 React 特性。
為什么要使用 React Hooks
狀態(tài)邏輯難以復用: 業(yè)務變得復雜之后,組件之間共享狀態(tài)變得頻繁,組件復用和狀態(tài)邏輯管理就變得十分復雜。使用 redux 也會加大項目的復雜度和體積。 組成復雜難以維護: 復雜的組件中有各種難以管理的狀態(tài)和副作用,在同一個生命周期中你可能會因為不同情況寫出各種不相關的邏輯,但實際上我們通常希望一個函數(shù)只做一件事情。 類的 this 指向性問題: 我們用 class 來創(chuàng)建 react 組件時,為了保證 this 的指向正確,我們要經(jīng)常寫這樣的代碼: const that = this,或者是this.handleClick = this.handleClick.bind(this)>;一旦 this 使用錯誤,各種 bug 就隨之而來。
為了解決這些麻煩,hooks 允許我們使用簡單的特殊函數(shù)實現(xiàn) class 的各種功能。
useState
在 React 組件中,我們經(jīng)常要使用 state 來進行數(shù)據(jù)的實時響應,根據(jù) state 的變化重新渲染組件更新視圖。
因為純函數(shù)不能有狀態(tài),在 hooks 中,useState就是一個用于為函數(shù)組件引入狀態(tài)(state)的狀態(tài)鉤子。
const?[state,?setState]?=?useState(initialState);
useState 的唯一參數(shù)是狀態(tài)初始值(initial state),它返回了一個數(shù)組,這個數(shù)組的第[0]項是當前當前的狀態(tài)值,第[1]項是可以改變狀態(tài)值的方法函數(shù)。
延遲初始化
initialState 參數(shù)是初始渲染期間使用的狀態(tài)。在隨后的渲染中,它會被忽略了。如果初始狀態(tài)是高開銷的計算結果,則可以改為提供函數(shù),該函數(shù)僅在初始渲染時執(zhí)行:
function?Counter({initialCount?=?0})?{
??//?初始值為1
??const?[count,?setCount]?=?useState(()?=>?initialCount?+?1);
??return?(
????<>
??????Count:?{count}
??????<button?onClick={()?=>?setCount(0)}>Resetbutton>
??????<button?onClick={()?=>?setCount(count?+?1)}>+button>
??????<button?onClick={()?=>?setCount(prevCount?=>?prevCount?-?1)}>-button>
????>
??);
}
函數(shù)式更新對比普通更新
如果需要使用前一時刻的 state(狀態(tài)) 計算新 state(狀態(tài)) ,則可以將 函數(shù) 傳遞給 setState 。該函數(shù)將接收先前 state 的值,并返回更新的 state。
那么setCount(newCount)和setCount(preCount => newCount)有什么區(qū)別呢,我們寫個例子來看下:
function?Counter()?{
??const?[count,?setCount]?=?useState(0);
??function?add()?{
????setTimeout(()?=>?{
??????setCount(count?+?1);
????},?3000);
??}
??function?preAdd(){
????setTimeout(()?=>?{
??????//?根據(jù)前一時刻的?count?設置新的?count
??????setCount(count?=>?count?+?1);
????},?3000);
??}
??//?監(jiān)聽?count?變化
??useEffect(()?=>?{
????console.log(count)
??},?[count])
??return?(
????<>
??????Count:?{count}
??????<button?onClick={add}>addbutton>
??????<button?onClick={preAdd}>preAddbutton>
????>
??);
}

我們首先快速點擊 add 按鈕三次,三秒后 count 變?yōu)?1;然后快速點擊 preAdd 三下,三秒后依次出現(xiàn)了 2、3、4。測試結果如下:

為什么setCount(count + 1)好像只執(zhí)行了一次呢,因為每次更新都是獨立的閉包,當點擊更新狀態(tài)的時候,函數(shù)組件都會重新被調(diào)用。 快速點擊時,當前 count 為 0,即每次點擊傳入的值都是相同的,那么得到的結果也是相同的,最后 count 變?yōu)?1 后不再變化。
為什么setCount(count => count + 1)好像能執(zhí)行三次呢,因為當傳入一個函數(shù)時,回調(diào)函數(shù)將接收當前的 state,并返回一個更新后的值。 三秒后,第一次setCount獲取到最新的 count 為 1,然后執(zhí)行函數(shù)將 count 變?yōu)?2,接著第二次獲取到當前 count 為 2,執(zhí)行函數(shù)將 count 變?yōu)榱?3。每次獲取到的最新 count 不一樣,最后結果自然也不同。
那么進行第二次實驗,我先快速點擊 preAdd 三下,然后接著快速點擊 add 按鈕三次,三秒后結果會怎么樣呢。根據(jù)以上結論猜測,preAdd 是根據(jù)最新值,所以 count 依次變?yōu)?1、2、3,然后 add 是傳入的當前 count 為 0,最后變?yōu)?1。最后結果應該是 1、2、3、1,測試結果正確:
useReducer
const?[state,?dispatch]?=?useReducer(reducer,?initialState,?initialFunc);
useReducer 可以接受三個參數(shù),第一個參數(shù)接收一個形如(state, action) => newState 的 reducer 純函數(shù),使用時可以通過dispatch(action)來修改相關邏輯。
第二個參數(shù)是 state 的初始值,它返回當前 state 以及發(fā)送 action 的 dispatch 函數(shù)。
你可以選擇惰性地創(chuàng)建初始 state,為此,需要將 init 函數(shù)作為 useReducer 的第三個參數(shù)傳入,這樣初始 state 將被設置為 init(initialArg)。
對比 useState 的優(yōu)勢
useReducer 是 React 提供的一個高級 Hook,它不像 useEffect、useState 等 hook 一樣必須,那么使用它有什么好處呢?如果使用 useReducer 改寫一下計數(shù)器例子:
//官方示例
function?countReducer(state,?action)?{
??switch?(action.type)?{
????case?'add':
??????return?state?+?1;
????case?'minus':
??????return?state?-?1;
????default:
??????return?state;
??}
}
function?initFunc(initialCount)?{
??return?initialCount?+?1;
}
function?Counter({initialCount?=?0})?{
??const?[count,?dispatch]?=?useReducer(countReducer,?initialCount,?initFunc);
??return?(
????<div>
??????<p>Count:?{count}p>
??????<button?onClick={()?=>?{?dispatch({?type:?'add'?});?}}?>
????????點擊+1
??????button>
??????<button?onClick={()?=>?{?dispatch({?type:?'minus'?});?}}?>
????????點擊-1
??????button>
????div>
??);
}
對比 useState 可知,看起來我們的代碼好像變得復雜了,但實際應用到復雜的項目環(huán)境中,將狀態(tài)管理和代碼邏輯放到一起管理,使我們的代碼具有更好的可讀性、可維護性和可預測性。
useEffect
useEffect(create,?deps);
useEffect()用來引入具有副作用的操作,最常見的就是向服務器請求數(shù)據(jù)。該 Hook 接收一個函數(shù),該函數(shù)會在組件渲染到屏幕之后才執(zhí)行。
和 react 類的生命周期相比,useEffect Hook 可以當做 componentDidMount,componentDidUpdate 和 componentWillUnmount 的組合。默認情況下,react 首次渲染和之后的每次渲染都會調(diào)用一遍傳給 useEffect 的函數(shù)。
useEffect 的性能問題
因為 React 首次渲染和之后的每次渲染都會調(diào)用一遍傳給 useEffect 的函數(shù),所以大多數(shù)情況下很有可能會產(chǎn)生性能問題。
為了解決這個問題,可以將數(shù)組作為可選的第二個參數(shù)傳遞給 useEffect。數(shù)組中可選擇性寫 state 中的數(shù)據(jù),代表只有當數(shù)組中的 state 發(fā)生變化是才執(zhí)行函數(shù)內(nèi)的語句,以此可以使用多個useEffect分離函數(shù)關注點。如果是個空數(shù)組,代表只執(zhí)行一次,類似于 componentDidUpdata。
解綁副作用
在 React 類中,經(jīng)常會需要在組件卸載時做一些事情,例如移除監(jiān)聽事件等。在 class 組件中,我們可以在 componentWillUnmount 這個生命周期中做這些事情,而在 hooks 中,我們可以通過 useEffect 第一個函數(shù)中 return 一個函數(shù)來實現(xiàn)相同效果。以下是一個簡單的清除定時器例子:
function?Counter()?{
??const?[count,?setCount]?=?useState(0);
??useEffect(()?=>?{
????const?timer?=?setInterval(()?=>?{
??????setCount(count?=>?count?+?1);
????},?1000);
????return?()?=>?clearInterval(timer);
??},?[]);
??return?(
????<>
??????Count:?{count}
????>
??);
}
useLayoutEffect
useLayoutEffect(create,?deps);
它和 useEffect 的結構相同,區(qū)別只是調(diào)用時機不同。
useEffect 在渲染時是異步執(zhí)行,要等到瀏覽器將所有變化渲染到屏幕后才會被執(zhí)行。 useLayoutEffect 會在 瀏覽器 layout 之后,painting 之前執(zhí)行, 可以使用 useLayoutEffect 來讀取 DOM 布局并同步觸發(fā)重渲染 盡可能使用標準的 useEffect 以避免阻塞視圖更新
useEffect 和 useLayoutEffect 的差別
為了更清晰的對比 useEffect 和 useLayoutEffect,我們寫個 demo 來看看兩種 hook 的效果:
function?Counter()?{
??function?delay(ms){
????const?startTime?=?new?Date().getTime();
????while?(new?Date().getTime()???}
??const?[count,?setCount]?=?useState(0);
??//?useLayoutEffect(()?=>?{
??//???console.log('useLayoutEffect:',?count)
??//???return?()?=>?console.log('useLayoutEffectDestory:',?count)
??//?},?[count]);
??useEffect(()?=>?{
????console.log('useEffect:',?count)
????//?延長一秒看效果
????if(count?===?5)?{
??????delay(1000)
??????setCount(count?=>?count?+?1)
????}
????return?()?=>?console.log('useEffectDestory:',?count)
??},?[count]);
??return?(
????<>
??????Count:?{count}
??????<button?onClick={()?=>?setCount(5)}>setbutton>
????>
??);
}
首先我們先看看 useEffect 的執(zhí)行效果:
useEffect 和 useEffectDestroy 的執(zhí)行順序也很好理解,先執(zhí)行了 useEffectDestroy 銷毀了 0,然后在 useEffect 修改 count 為 5,這時,count 可見已經(jīng)變成了 5,然后銷毀 5,設置 count 為 6,然后渲染 6。
整個渲染過程可以很明顯的看到 count 0->5->6 的過程,如果在實際項目中,這種情況會出現(xiàn)閃屏效果,很影響用戶體驗。因為useEffect 在渲染時是異步執(zhí)行,并且要等到瀏覽器將所有變化渲染到屏幕后才會被執(zhí)行,所以,我們盡量不要在 useEffect 里面進行 DOM 操作。
再將 setCount 操作放到 useLayoutEffect 里的執(zhí)行看看效果:
useLayoutEffect 和 useLayoutEffectDestroy 的執(zhí)行順序和 useEffect 一樣,都是在下一次操作之前先銷毀,但是整個渲染過程和 useEffect 明顯不一樣。雖然在打印的 useLayoutEffect 中有明顯停頓,但在渲染過程只能看到 count 0->6 的過程,這是因為 useLayoutEffect 的同步特性,會在瀏覽器渲染之前同步更新 DOM 數(shù)據(jù),哪怕是多次的操作,也會在渲染前一次性處理完,再交給瀏覽器繪制。這樣不會導致閃屏現(xiàn)象發(fā)生,但是會阻塞視圖的更新。。
最后,我們同時看看兩個 setCout 分別在兩個 hook 的執(zhí)行時機;
在 useEffect 執(zhí)行效果:
在 useLayoutEffect 執(zhí)行效果:
我們可以發(fā)現(xiàn)無論在哪兒執(zhí)行 setCount,hooks 的先后順序都不變,始終是先 useLayoutEffect 銷毀,然后 useLayoutEffect 執(zhí)行,再然后才是 useEffect 銷毀,useEffect 執(zhí)行。但是頁面渲染的不同和打印時的明顯卡頓,我們知道 hooks 的執(zhí)行時機應該是useLayoutEffectDestory -> useLayoutEffect -> 渲染 -> useEffectDestory -> useEffect。
useMemo
const?memoizedValue?=?useMemo(()?=>?computeExpensiveValue(a,?b),?[a,?b]);
把“創(chuàng)建”函數(shù)和依賴項數(shù)組作為參數(shù)傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優(yōu)化有助于避免在每次渲染時都進行高開銷的計算。
useMemo 和 useEffect 的區(qū)別
useMemo 看起來和 useEffect 很像,但是如果你想在 useMemo 里面 setCount 或者其他修改了 DOM 的操作,那你可能會遇到一些問題。因為傳入 useMemo 的函數(shù)會在渲染期間執(zhí)行,你可能看不到想要的效果,所以請不要在這個函數(shù)內(nèi)部執(zhí)行與渲染無關的操作。
useMemo 還返回一個 memoized 值,之后僅會在某個依賴項改變時才重新計算 memoized 值。這種優(yōu)化有助于避免在每次渲染時都進行高開銷的計算,具體應用看以下例子:
function?Counter()?{
??const?[count,?setCount]?=?useState(1);
??const?[val,?setValue]?=?useState('');
??const?getNum?=?()?=>?{
????console.log('compute');
????let?sum?=?0;
????for?(let?i?=?0;?i?100;?i++)?{
??????sum?+=?i;
????}
????return?sum;
??}
??const?memoNum?=?useMemo(()?=>?getNum(),?[count])
??return?<div>
????<h4>總和:{getNum()}?{memoNum}h4>
????<div>
??????<button?onClick={()?=>?setCount(count?+?1)}>+1button>
??????<input?value={val}?onChange={event?=>?setValue(event.target.value)}/>
????div>
??div>;
}
useMemo 效果:
正常情況下,當你在 input 框輸入時,因為修改了 val,所以頁面會重新渲染,那么就需要重新計算 getNum,但使用 useMemo 后,因為依賴的 count 沒變,則 memoNum 不會重新計算。
useCallback
const?memoizedCallback?=?useCallback(
??()?=>?{
????doSomething(a,?b);
??},
??[a,?b],
);
返回一個 memoized 回調(diào)函數(shù)。
把內(nèi)聯(lián)回調(diào)函數(shù)及依賴項數(shù)組作為參數(shù)傳入 useCallback,它將返回該回調(diào)函數(shù)的 memoized 版本,該回調(diào)函數(shù)僅在某個依賴項改變時才會更新。當你把回調(diào)函數(shù)傳遞給經(jīng)過優(yōu)化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將非常有用。
useCallback(fn, deps)相當于useMemo(() => fn, deps)。
useRef
const?refContainer?=?useRef(initialValue);
類組件、React 元素用 React.createRef,函數(shù)組件使用 useRef useRef 返回一個可變的 ref 對象,其 current 屬性被初始化為傳入的參數(shù)(initialValue useRef 返回的 ref 對象在組件的整個生命周期內(nèi)保持不變,也就是說每次重新渲染函數(shù)組件時,返回的 ref 對象都是同一個(使用 React.createRef ,每次重新渲染組件都會重新創(chuàng)建 ref)
//?官網(wǎng)例子
function?TextInputWithFocusButton()?{
??const?inputEl?=?useRef(null);
??const?onButtonClick?=?()?=>?{
????//?`current`?指向已掛載到?DOM?上的文本輸入元素
????inputEl.current.focus();
??};
??return?(
????<>
??????
??????
????>
??);
}
useImperativeHandle
useImperativeHandle(ref,?createHandle,?[deps])
useImperativeHandle 可以讓你在使用 ref 時自定義暴露給父組件的實例值。
如下,渲染 的父組件可以調(diào)用 inputRef.current.focus():
//?官網(wǎng)例子
function?FancyInput(props,?ref)?{
??const?inputRef?=?useRef();
??useImperativeHandle(ref,?()?=>?({
????focus:?()?=>?{
??????inputRef.current.focus();
????}
??}));
??return?<input?ref={inputRef}?...?/>;
}
FancyInput?=?forwardRef(FancyInput);
useContext
在 hooks 中,組件都是函數(shù),所以我們可以通過參數(shù)的方式進行傳值,但是有時候我們也會遇到兄弟組件和爺孫組件之間的傳值,這時候通過函數(shù)參數(shù)傳值就不太方便了。hooks 提供了 useContext(共享狀態(tài)鉤子)來解決這個問題。
useContext 接受一個 context 對象(從 React.createContext 返回的值)并返回當前 context 值,由最近 context 提供程序給 context 。
當組件上層最近的 更新時,該 Hook 會觸發(fā)重渲染,并使用最新傳遞給 Context provider 的 context value 值。
在 hooks 中使用 content,需要使用 createContext,useContext:
//?context.js??新建一個context
import?{?createContext?}?from?'react';
const?AppContext?=?React.createContext({});
//?HooksContext.jsx??父組件,提供context
import?React,?{?useState?}?from?'react';
import?AppContext?from?'./context';
function?HooksContext()?{
??const?[count,?setCnt]?=?useState(0);
??const?[age,?setAge]?=?useState(16);
??return?(
????<div>
??????<p>年齡{age}p>
??????<p>你點擊了{count}次p>
??????<AppContext.Provider?value={{?count,?age?}}>
????????<div?className="App">
??????????<Navbar?/>
??????????<Messages?/>
????????div>
??????AppContext.Provider>
????div>
??);
}
//?子組件,使用context
import?React,?{?useContext?}?from?'react';
import?AppContext?from?'./context';
const?Navbar?=?()?=>?{
??const?{?count,?age?}?=?useContext(AppContext);
??return?(
????<div?className="navbar">
??????<p>使用contextp>
??????<p>年齡{age}p>
??????<p>點擊了{count}次p>
????div>
??);
}
構建自定義 Hook
當我們想要在兩個 JavaScript 函數(shù)之間共享邏輯時,我們會將共享邏輯提取到第三個函數(shù)。組件和 Hook 都是函數(shù),所以通過這種辦法可以調(diào)用其他 Hook。
例如,我們可以把判斷朋友是否在線的功能抽出來,新建一個 useFriendStatus 的 hook 專門用來判斷某個 id 是否在線:
//?官網(wǎng)例子
import?{?useState,?useEffect?}?from?'react';
function?useFriendStatus(friendID)?{
??const?[isOnline,?setIsOnline]?=?useState(null);
??function?handleStatusChange(status)?{
????setIsOnline(status.isOnline);
??}
??useEffect(()?=>?{
????ChatAPI.subscribeToFriendStatus(friendID,?handleStatusChange);
????return?()?=>?{
??????ChatAPI.unsubscribeFromFriendStatus(friendID,?handleStatusChange);
????};
??});
??return?isOnline;
}
這時候我們就可以在需要 FriendStatus 組件的地方為所欲為、為所欲為:
function?FriendStatus(props)?{
??const?isOnline?=?useFriendStatus(props.friend.id);
??if?(isOnline?===?null)?{
????return?'Loading...';
??}
??return?isOnline???'Online'?:?'Offline';
}
function?FriendListItem(props)?{
??const?isOnline?=?useFriendStatus(props.friend.id);
??return?(
????<li?style={{?color:?isOnline???'green'?:?'black'?}}>
??????{props.friend.name}
????li>
??);
}
簡單總結
| hook | 功能 |
|---|---|
| useState | 設置和改變 state,代替原來的 state 和 setState |
| useReducer | 代替原來 redux 里的 reducer,方便管理狀態(tài)邏輯 |
| useEffect | 引入具有副作用的操作,類比原來的生命周期 |
| useLayoutEffect | 與 useEffect 作用相同,但它會同步調(diào)用 effect |
| useMemo | 可根據(jù)狀態(tài)變化控制方法執(zhí)行,優(yōu)化無用渲染,提高性能 |
| useCallback | 類似 useMemo,useMemo 優(yōu)化傳值,usecallback 優(yōu)化傳入的方法 |
| useContext | 上下文爺孫組件及更深層組件傳值 |
| useRef | 返回一個可變的 ref 對象 |
| useImperativeHandle | 可以讓你在使用 ref 時自定義暴露給父組件的實例值 |
參考文章
React Hooks
React Hooks 入門教程 - 阮一峰
React Hooks 詳解 【近 1W 字】+ 項目實戰(zhàn)
●?JavaScript 測試系列實戰(zhàn)(四):掌握 React Hooks 測試技巧
●?用動畫和實戰(zhàn)打開 React Hooks(一):useState 和 useEffect
●?Taro 小程序開發(fā)大型實戰(zhàn)(五):使用 Hooks 版的 Redux 實現(xiàn)應用狀態(tài)管理(下篇)
·END·
匯聚精彩的免費實戰(zhàn)教程
喜歡本文,點個“在看”告訴我


