ReactHooks由淺入深:所有 hooks 的梳理、匯總與解析
Vue 中的指令、React 中的 hooks 都是框架的核心知識點。但是對于很多同學來說,因為日常工作中開發的局限性,所以對這些 指令 或 hooks 認知的并不全面,一旦在面試的時候被問到不熟悉的 指令 或者 hooks 可能就會吃虧。
所以說,咱們今天就先來整理一下 React 中的 hooks,整理的過程會 由淺入深 同時配合一些代碼進行說明。爭取讓哪怕不是很熟悉 react 的同學,也可以在本文章中有一定的收獲。
一、React 基礎普及
1、什么是 react 中的組件
在 React 16.8 之后,react 使用 函數 表示組件,稱為 函數式組件。
例如以下代碼,就是兩個基礎函數式組件(App 與 Greeting):
import React from 'react';
// 函數組件
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
// 使用函數組件
function App() {
return (
<div>
<Greeting name="Alice" />
<Greeting name="Bob" />
<Greeting name="Charlie" />
</div>
);
}
export default App;
2、什么是 react hooks
在 React 中 以 use 開頭的函數 就被稱之為 hooks。
React 默認提供一些 hooks。同樣的,我們也可以自定義一些函數(以 use)開頭,那么該函數就可以被稱為是 hooks。
import { useState } from 'react';
// 最簡單的自定義Hooks
function useMyCustomHook() {
// 在這個例子中,我們只是返回一個固定的值
const [value] = useState("Hello, World!");
return value;
}
export default useMyCustomHook;
3、hooks 解決了什么問題?
如果沒有Hooks,函數組件的功能相對有限,只能接受 Props、渲染UI,以及響應來自父組件的事件。
因此,Hooks的出現主要是為了解決是哪個問題:
- 強化函數式組件:讓函數組件具備類組件(16.8之前)的功能,可以有自己的狀態、處理副作用、獲取ref,以及進行數據緩存。
- 提高復用性: hooks本質上是以 use 開頭的函數,通過函數封裝邏輯,解決邏輯復用的問題。
- 函數式編程:react 推薦函數式編程,摒棄面向對象編程。
二、React Hooks 劃分
React Hooks 根據性能可以劃分為 5 大類:
1.1 狀態處理:useState
useState是React提供的一個Hook,它讓函數組件也能像類組件一樣擁有狀態。通過useState,你可以讓組件在內部管理一些數據,并在數據更新時重新渲染視圖。
在使用useState時,你會得到一個包含兩個值的數組:
- state:當前狀態的值,它用于提供給UI,作為渲染視圖的數據源。
- dispatch:一個函數,用于改變state的值,從而觸發函數組件的重新渲染。
useState的基礎用法如下:
const DemoState = (props) => {
// number是當前state的值,setNumber是用于更新state的函數
let [number, setNumber] = useState(0) // 0為初始值
return (
<div>
<span>{number}</span>
<button onClick={() => {
setNumber(number + 1)
console.log(number) // 這里的number是不能夠即時改變的
}}>增加</button>
</div>
)
}
在使用useState時需要注意:
- 在同一個函數組件的執行過程中,state的值是固定不變的。比如,如果你在一個定時器中改變state的值,組件不會重新渲染。
- 如果兩次dispatch傳入相同的state值,那么組件就不會更新,因為React會認為狀態沒有改變。
- 當你在當前執行上下文中觸發dispatch時,無法立即獲取到最新的state值,只有在下一次組件重新渲染時才能獲取到。
1.2 狀態處理:useReducer
useReducer是React Hooks提供的一個功能,類似于Redux的狀態管理工具。
在使用useReducer時,你會得到一個包含兩個值的數組:
- state:state是更新后的狀態值
- dispatch:dispatch是用于派發更新的函數,與useState中的dispatch函數類似
基礎用法如下:
const DemoUseReducer = () => {
// number為更新后的state值, dispatchNumbner為當前的派發函數
const [number, dispatchNumber] = useReducer((state, action) => {
const { payload, name } = action;
// 根據不同的action類型來更新state
switch (name) {
case 'add':
return state + 1;
case 'sub':
return state - 1;
case 'reset':
return payload;
default:
return state;
}
}, 0);
return (
<div>
當前值:{number}
{/* 派發更新 */}
<button onClick={() => dispatchNumber({ name: 'add' })}>增加</button>
<button onClick={() => dispatchNumber({ name: 'sub' })}>減少</button>
<button onClick={() => dispatchNumber({ name: 'reset', payload: 666 })}>賦值</button>
{/* 把dispatch和state傳遞給子組件 */}
<MyChildren dispatch={dispatchNumber} state={{ number }} />
</div>
);
};
在useReducer中,你需要傳入一個reducer函數,這個函數接受當前的state和一個action作為參數,并返回新的state。如果新的state和之前的state指向的是同一個內存地址,那么組件就不會更新。
1.3 狀態處理:useSyncExternalStore
useSyncExternalStore的出現與React版本18中更新模式下外部數據的撕裂(tearing)密切相關。它允許React組件在并發模式下安全有效地讀取外部數據源,并在組件渲染過程中檢測數據的變化,以及在數據源發生變化時調度更新,以確保結果的一致性。
基礎介紹如下:
useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot
)
- subscribe是一個訂閱函數,當數據發生變化時會觸發該函數。useSyncExternalStore會使用帶有記憶功能的getSnapshot來判斷數據是否發生變化,如果有變化,則強制更新數據。
- getSnapshot可以看作是帶有記憶功能的選擇器。當數據源變化時,通過getSnapshot生成新的狀態值,該狀態值可作為組件的數據源使用。getSnapshot能夠檢查訂閱的值是否發生變化,一旦發生變化,則觸發更新。
- getServerSnapshot用于hydration模式下的getSnapshot。
基礎用法示例如下:
import { combineReducers, createStore } from 'redux';
/* number Reducer */
function numberReducer(state = 1, action) {
switch (action.type) {
case 'ADD':
return state + 1;
case 'DEL':
return state - 1;
default:
return state;
}
}
/* 注冊reducer */
const rootReducer = combineReducers({ number: numberReducer });
/* 創建 store */
const store = createStore(rootReducer, { number: 1 });
function Index() {
/* 訂閱外部數據源 */
const state = useSyncExternalStore(store.subscribe, () => store.getState().number);
console.log(state);
return (
<div>
{state}
<button onClick={() => store.dispatch({ type: 'ADD' })}>點擊</button>
</div>
);
}
當點擊按鈕時,會觸發reducer,然后會觸發store.subscribe訂閱函數,執行getSnapshot得到新的number,判斷number是否發生變化,如果有變化,則觸發更新。
1.4 狀態處理:useTransition
在React v18中,引入了一種新概念叫做過渡任務。這些任務與立即更新任務相對應,通常指的是一些不需要立即響應的更新,比如頁面從一個狀態過渡到另一個狀態。
舉個例子,當用戶點擊tab從tab1切換到tab2時,會產生兩個更新任務:
- 第一個任務是hover狀態從tab1變成tab2。
- 第二個任務是內容區域從tab1內容變換到tab2內容。
這兩個任務中,用戶通常希望hover狀態的響應更迅速,而內容的響應可能需要更長時間,比如請求數據等操作。因此,第一個任務可以視為立即執行任務,而第二個任務則可以視為過渡任務。
useTransition的基礎介紹如下:
import { useTransition } from 'react';
/* 使用 */
const [isPending, startTransition] = useTransition();
useTransition會返回一個數組,其中包含兩個值:
- 第一個值是一個標志,表示是否處于過渡狀態。
- 第二個值是一個函數,類似于上述的startTransition,用于將更新任務轉換為過渡任務。
在基礎用法中,除了切換tab的場景外,還有很多其他場景適合產生過渡任務,比如實時搜索并展示數據。這種情況下,有兩個優先級的任務:第一個是受控表單的實時響應,第二個是輸入內容改變后數據展示的變化。
下面是一個基本使用useTransition的示例:
const mockList1 = new Array(10000).fill('tab1').map((item,index)=>item+'--'+index )
const mockList2 = new Array(10000).fill('tab2').map((item,index)=>item+'--'+index )
const mockList3 = new Array(10000).fill('tab3').map((item,index)=>item+'--'+index )
const tab = {
tab1: mockList1,
tab2: mockList2,
tab3: mockList3
}
export default function Index(){
const [ active, setActive ] = React.useState('tab1') //立即響應的任務,立即更新任務
const [ renderData, setRenderData ] = React.useState(tab[active]) //不需要立即響應的任務,過渡任務
const [ isPending,startTransition ] = React.useTransition()
const handleChangeTab = (activeItem) => {
setActive(activeItem) //立即更新
startTransition(()=>{ //startTransition里面的任務優先級低
setRenderData(tab[activeItem])
})
}
return <div>
<div className='tab' >
{ Object.keys(tab).map((item)=> <span className={ active === item && 'active' } onClick={()=>handleChangeTab(item)} >{ item }</span> ) }
</div>
<ul className='content' >
{ isPending && <div> loading... </div> }
{ renderData.map(item=> <li key={item} >{item}</li>) }
</ul>
</div>
}
以上示例中,當切換tab時,會產生兩個優先級任務:第一個任務是setActive控制tab的active狀態的改變,第二個任務是setRenderData控制渲染的長列表數據(在實際場景中,這可能是一些數據量大的可視化圖表)。
1.5 狀態處理:useDeferredValue
在React 18中,引入了useDeferredValue,它可以讓狀態的更新滯后于派生。useDeferredValue的實現效果類似于transition,在緊急任務執行后,再得到新的狀態,這個新的狀態就稱之為DeferredValue。
useDeferredValue基礎介紹:
useDeferredValue和前面提到的useTransition有什么異同呢?
相同點: useDeferredValue和useTransition本質上都是標記為過渡更新任務。
不同點: useTransition將內部的更新任務轉換為過渡任務transition,而useDeferredValue則是通過過渡任務得到新的值,這個值作為延遲狀態。一個是處理一段邏輯,另一個是生成一個新的狀態。
useDeferredValue接受一個參數value,通常是可變的state,返回一個延遲狀態deferredValue。
const deferredValue = React.useDeferredValue(value)
useDeferredValue基礎用法:
下面將上面的例子改用useDeferredValue來實現。
export default function Index(){
const [ active, setActive ] = React.useState('tab1') //需要立即響應的任務,立即更新任務
const deferredActive = React.useDeferredValue(active) // 將狀態延遲更新,類似于過渡任務
const handleChangeTab = (activeItem) => {
setActive(activeItem) // 立即更新
}
const renderData = tab[deferredActive] // 使用延遲狀態
return <div>
<div className='tab' >
{ Object.keys(tab).map((item)=> <span className={ active === item && 'active' } onClick={()=>handleChangeTab(item)} >{ item }</span> ) }
</div>
<ul className='content' >
{ renderData.map(item=> <li key={item} >{item}</li>) }
</ul>
</div>
}
上述代碼中,active是正常改變的狀態,deferredActive是延遲的active狀態。我們使用正常狀態來改變tab的active狀態,而使用延遲狀態來更新視圖,從而提升了用戶體驗。
2.1 副作用執行:useEffect
React hooks提供了API,用于彌補函數組件沒有生命周期的不足。主要利用了hooks中的useEffect、useLayoutEffect和useInsertionEffect。其中,最常用的是useEffect。現在我們來看一下useEffect的使用。
useEffect基礎介紹:
useEffect(() => {
return cleanup;
}, dependencies);
useEffect的第一個參數是一個回調函數,返回一個清理函數cleanup。cleanup函數會在下一次回調函數執行之前調用,用于清除上一次回調函數產生的副作用。
第二個參數是一個依賴項數組,里面可以包含多個依賴項。當依賴項發生變化時,會執行上一次callback返回的cleanup函數,并執行新的effect回調函數。
對于useEffect的執行,React采用了異步調用的處理邏輯。對于每個effect的回調函數,React會將其放入任務隊列中,類似于setTimeout回調函數的方式,等待主線程任務完成、DOM更新、JS執行完成以及視圖繪制完成后才執行。因此,effect回調函數不會阻塞瀏覽器的視圖繪制。
useEffect基礎用法:
/* 模擬數據交互 */
function getUserInfo(a){
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({
name:a,
age:16,
})
},500)
})
}
const Demo = ({ a }) => {
const [userMessage, setUserMessage] = useState({});
const div = useRef();
const [number, setNumber] = useState(0);
/* 模擬事件監聽處理函數 */
const handleResize = () => {};
/* useEffect使用 */
useEffect(() => {
/* 請求數據 */
getUserInfo(a).then(res => {
setUserMessage(res);
});
/* 定時器 延時器等 */
const timer = setInterval(() => console.log(666), 1000);
/* 操作dom */
console.log(div.current); /* div */
/* 事件監聽等 */
window.addEventListener('resize', handleResize);
/* 此函數用于清除副作用 */
return function() {
clearInterval(timer);
window.removeEventListener('resize', handleResize);
};
/* 只有當props->a和state->number改變的時候, useEffect副作用函數重新執行,如果此時數組為空[],證明函數只有在初始化的時候執行一次,相當于componentDidMount */
}, [a, number]);
return (
<div ref={div}>
<span>{userMessage.name}</span>
<span>{userMessage.age}</span>
<div onClick={() => setNumber(1)}>{number}</div>
</div>
);
};
上述代碼中,在useEffect中做了以下功能:
- 請求數據。
- 設置定時器、延時器等。
- 操作DOM,在React Native中可以通過ref獲取元素位置信息等內容。
- 注冊事件監聽器,在React Native中可以注冊NativeEventEmitter。
- 清除定時器、延時器、解綁事件監聽器等。
2.2 副作用執行:useLayoutEffect
useLayoutEffect基礎介紹:
useLayoutEffect和useEffect的不同之處在于它采用了同步執行的方式。那么它和useEffect有什么區別呢?
① 首先,useLayoutEffect在DOM更新之后、瀏覽器繪制之前執行。這使得我們可以方便地修改DOM、獲取DOM信息,從而避免了不必要的瀏覽器回流和重繪。相比之下,如果將DOM布局修改放在useEffect中,那么useEffect的執行是在瀏覽器繪制視圖之后進行的,接著再去修改DOM,可能會導致瀏覽器進行額外的回流和重繪。由于兩次繪制,可能會導致視圖上出現閃現或突兀的效果。
② useLayoutEffect回調函數中的代碼執行會阻塞瀏覽器的繪制。
useLayoutEffect基礎用法:
const DemoUseLayoutEffect = () => {
const target = useRef();
useLayoutEffect(() => {
/* 在DOM繪制之前,移動DOM到指定位置 */
const { x, y } = getPositon(); // 獲取要移動的x,y坐標
animate(target.current, { x, y });
}, []);
return (
<div>
<span ref={target} className="animate"></span>
</div>
);
};
2.3 副作用執行:useInsertionEffect
useInsertionEffect基礎介紹:
useInsertionEffect是React v18新增的hooks之一,其用法與useEffect和useLayoutEffect相似。那么這個hooks用于什么呢?
在介紹useInsertionEffect用途之前,先來看一下useInsertionEffect的執行時機。
React.useEffect(() => {
console.log('useEffect 執行');
}, []);
React.useLayoutEffect(() => {
console.log('useLayoutEffect 執行');
}, []);
React.useInsertionEffect(() => {
console.log('useInsertionEffect 執行');
}, []);
打印結果為:useInsertionEffect執行 -> useLayoutEffect執行 -> useEffect執行。
可以看到,useInsertionEffect的執行時機要比useLayoutEffect提前。在useLayoutEffect執行時,DOM已經更新了,但是在useInsertionEffect執行時,DOM還沒有更新。useInsertionEffect主要是解決CSS-in-JS在渲染中注入樣式的性能問題。這個hooks主要適用于這個場景,在其他場景下React不建議使用這個hooks。
useInsertionEffect模擬使用:
export default function Index() {
React.useInsertionEffect(() => {
/* 動態創建style標簽插入到head中 */
const style = document.createElement('style');
style.innerHTML = `
.css-in-js {
color: red;
font-size: 20px;
}
`;
document.head.appendChild(style);
}, []);
return <div className="css-in-js">hello, useInsertionEffect</div>;
}
上述代碼模擬了useInsertionEffect的使用。
3.1 狀態傳遞:useContext
useContext基礎介紹
可以使用useContext來獲取父級組件傳遞過來的context值,這個值是最近的父級組件Provider設置的value值。useContext的參數通常是由createContext方式創建的context對象,也可以是父級上下文context傳遞的(參數為context)。useContext可以代替context.Consumer來獲取Provider中保存的value值。
const contextValue = useContext(context);
useContext接受一個參數,一般是context對象,返回值是context對象內部保存的value值。
useContext基礎用法:
/* 用useContext方式 */
const DemoContext = () => {
const value = useContext(Context);
/* my name is alien */
return <div> my name is {value.name}</div>;
}
/* 用Context.Consumer方式 */
const DemoContext1 = () => {
return (
<Context.Consumer>
{/* my name is alien */}
{value => <div> my name is {value.name}</div>}
</Context.Consumer>
);
}
export default () => {
return (
<div>
<Context.Provider value={{ name: 'alien', age: 18 }}>
<DemoContext />
<DemoContext1 />
</Context.Provider>
</div>
);
}
3.2 狀態傳遞:useRef
useRef基礎介紹:
useRef可以用來獲取元素,緩存狀態。它接受一個初始狀態initState作為初始值,并返回一個ref對象cur。cur對象上有一個current屬性,該屬性就是ref對象需要獲取的內容。
const cur = React.useRef(initState);
console.log(cur.current);
useRef基礎用法:
獲取DOM元素: 在React中,可以利用useRef來獲取DOM元素。在React Native中雖然沒有DOM元素,但是同樣可以利用useRef來獲取組件的節點信息(Fiber信息)。
const DemoUseRef = () => {
const dom = useRef(null);
const handleSubmit = () => {
console.log(dom.current); // <div>表單組件</div> DOM節點
}
return (
<div>
{/* ref標記當前DOM節點 */}
<div ref={dom}>表單組件</div>
<button onClick={handleSubmit}>提交</button>
</div>
);
}
保存狀態: 可以利用useRef返回的ref對象來保存狀態,只要當前組件不被銷毀,狀態就會一直存在。
const status = useRef(false);
/* 改變狀態 */
const handleChangeStatus = () => {
status.current = true;
}
3.3 狀態傳遞:useImperativeHandle
useImperativeHandle基礎介紹:
useImperativeHandle配合forwardRef可以自定義向父組件暴露的實例值。對于函數組件,如果我們想讓父組件能夠獲取子組件的實例,就可以使用useImperativeHandle和forwardRef來實現。
useImperativeHandle接受三個參數:
- 第一個參數ref:接受forwardRef傳遞過來的ref。
- 第二個參數createHandle:處理函數,返回值作為暴露給父組件的ref對象。
- 第三個參數deps:依賴項deps,當依賴項改變時形成新的ref對象。
useImperativeHandle基礎用法:
我們通過一個示例來說明,使用useImperativeHandle使得父組件能夠控制子組件中的input自動聚焦并設置值。
function Son(props, ref) {
const inputRef = useRef(null);
const [inputValue, setInputValue] = useState('');
useImperativeHandle(ref, () => {
const handleRefs = {
onFocus() {
inputRef.current.focus();
},
onChangeValue(value) {
setInputValue(value);
}
};
return handleRefs;
}, []);
return (
<div>
<input
placeholder="請輸入內容"
ref={inputRef}
value={inputValue}
/>
</div>
);
}
const ForwardSon = forwardRef(Son);
class Index extends React.Component {
inputRef = null;
handleClick() {
const { onFocus, onChangeValue } = this.inputRef;
onFocus();
onChangeValue('let us learn React!');
}
render() {
return (
<div style={{ marginTop: '50px' }}>
<ForwardSon ref={(node) => (this.inputRef = node)} />
<button onClick={this.handleClick.bind(this)}>操控子組件</button>
</div>
);
}
}
4.1 性能優化:useMemo
useMemo基礎介紹:
useMemo 可以在函數組件的渲染過程中同步執行一個函數邏輯,并將其返回值作為一個新的狀態進行緩存。這個 hooks 的作用在于優化性能,避免不必要的重復計算或渲染。
基本語法:
const cachedValue = useMemo(create, deps)
- create: 第一個參數是一個函數,函數的返回值將作為緩存值。
- deps: 第二個參數是一個數組,包含當前 useMemo 的依賴項。當這些依賴項發生變化時,會重新執行 create 函數以獲取新的緩存值。
- cachedValue: 返回值,為 create 函數的返回值或上一次的緩存值。
基本用法:
1. 派生新狀態:
function Scope() {
const keeper = useKeep()
const { cacheDispatch, cacheList, hasAliveStatus } = keeper
const contextValue = useMemo(() => {
return {
cacheDispatch: cacheDispatch.bind(keeper),
hasAliveStatus: hasAliveStatus.bind(keeper),
cacheDestory: (payload) => cacheDispatch.call(keeper, { type: ACTION_DESTORY, payload })
}
}, [keeper])
return (
<KeepaliveContext.Provider value={contextValue}>
</KeepaliveContext.Provider>
)
}
在上面的示例中,通過 useMemo 派生出一個新的狀態 contextValue,只有 keeper 發生變化時,才會重新生成 contextValue。
2. 緩存計算結果:
function Scope(){
const style = useMemo(()=>{
let computedStyle = {}
// 大量的計算
return computedStyle
},[])
return <div style={style} ></div>
}
在這個例子中,通過 useMemo 緩存了一個計算結果 style,只有當依賴項發生變化時,才會重新計算 style。
3. 緩存組件,減少子組件重渲染次數:
function Scope ({ children }){
const renderChild = useMemo(()=>{ children() },[ children ])
return <div>{ renderChild } </div>
}
通過 useMemo 緩存了子組件的渲染結果 renderChild,只有當 children 發生變化時,才會重新執行子組件的渲染。
4.2 性能優化:useCallback
useCallback基礎介紹:
useCallback 和 useMemo 接收的參數類似,都是在其依賴項發生變化后才執行,都返回緩存的值。但是它們的區別在于,useMemo 返回的是函數運行的結果,而 useCallback 返回的是一個經過處理的函數本身。它主要用于優化性能,避免不必要的函數重新創建,特別是在向子組件傳遞函數時,避免因為函數重新創建而導致子組件不必要的重新渲染。
基本用法:
const cachedCallback = useCallback(callbackFunction, deps)
- callbackFunction: 第一個參數是一個函數,需要進行緩存的函數。
- deps: 第二個參數是一個數組,包含當前 useCallback 的依賴項。當這些依賴項發生變化時,會重新創建新的緩存函數。
- cachedCallback: 返回值,為經過處理的緩存函數。
基礎用法示例:
const DemoChildren = React.memo((props)=>{
console.log('子組件更新')
useEffect(()=>{
props.getInfo('子組件')
},[])
return <div>子組件</div>
})
const DemoUseCallback=({ id })=>{
const [number, setNumber] = useState(1)
const getInfo = useCallback((sonName)=>{
console.log(sonName)
}, [id]) // 只有當 id 發生變化時,才會重新創建 getInfo 函數
return (
<div>
<button onClick={ ()=>setNumber(number+1) }>增加</button>
<DemoChildren getInfo={getInfo} />
</div>
)
}
在上面的示例中,getInfo 函數通過 useCallback 進行了緩存,只有當 id 發生變化時,才會重新創建 getInfo 函數。這樣可以避免因為函數重新創建而導致子組件不必要的重新渲染。
5.1 工具類:useId
在組件的頂層調用 useId 生成唯一 ID:
import { useId } from 'react';
function PasswordField() {
const passwordHintId = useId();
// ...
5.2 工具類:useDebugValue
在你的 自定義 Hook 的頂層調用 useDebugValue,以顯示可讀的調試值:
import { useDebugValue } from 'react';
function useOnlineStatus() {
// ...
useDebugValue(isOnline ? 'Online' : 'Offline');
// ...
}