Vue 中的指令、React 中的 hooks 都是框架的核心知識點。但是對于很多同學(xué)來說,因為日常工作中開發(fā)的局限性,所以對這些 指令 或 hooks 認(rèn)知的并不全面,一旦在面試的時候被問到不熟悉的 指令 或者 hooks 可能就會吃虧。
所以說,咱們今天就先來整理一下 React 中的 hooks,整理的過程會 由淺入深 同時配合一些代碼進(jìn)行說明。爭取讓哪怕不是很熟悉 react 的同學(xué),也可以在本文章中有一定的收獲。
在 React 16.8 之后,react 使用 函數(shù) 表示組件,稱為 函數(shù)式組件。
例如以下代碼,就是兩個基礎(chǔ)函數(shù)式組件(App 與 Greeting):
import React from 'react';// 函數(shù)組件function Greeting(props) { return <h1>Hello, {props.name}!</h1>;}// 使用函數(shù)組件function App() { return ( <div> <Greeting name="Alice" /> <Greeting name="Bob" /> <Greeting name="Charlie" /> </div> );}export default App;
在 React 中 以 use 開頭的函數(shù) 就被稱之為 hooks。
React 默認(rèn)提供一些 hooks。同樣的,我們也可以自定義一些函數(shù)(以 use)開頭,那么該函數(shù)就可以被稱為是 hooks。
import { useState } from 'react';// 最簡單的自定義Hooksfunction useMyCustomHook() { // 在這個例子中,我們只是返回一個固定的值 const [value] = useState("Hello, World!"); return value;}export default useMyCustomHook;
如果沒有Hooks,函數(shù)組件的功能相對有限,只能接受 Props、渲染UI,以及響應(yīng)來自父組件的事件。
因此,Hooks的出現(xiàn)主要是為了解決是哪個問題:
React Hooks 根據(jù)性能可以劃分為 5 大類:
useState是React提供的一個Hook,它讓函數(shù)組件也能像類組件一樣擁有狀態(tài)。通過useState,你可以讓組件在內(nèi)部管理一些數(shù)據(jù),并在數(shù)據(jù)更新時重新渲染視圖。
在使用useState時,你會得到一個包含兩個值的數(shù)組:
useState的基礎(chǔ)用法如下:
const DemoState = (props) => { // number是當(dāng)前state的值,setNumber是用于更新state的函數(shù) let [number, setNumber] = useState(0) // 0為初始值 return ( <div> <span>{number}</span> <button onClick={() => { setNumber(number + 1) console.log(number) // 這里的number是不能夠即時改變的 }}>增加</button> </div> )}
在使用useState時需要注意:
useReducer是React Hooks提供的一個功能,類似于Redux的狀態(tài)管理工具。
在使用useReducer時,你會得到一個包含兩個值的數(shù)組:
基礎(chǔ)用法如下:
const DemoUseReducer = () => { // number為更新后的state值, dispatchNumbner為當(dāng)前的派發(fā)函數(shù) const [number, dispatchNumber] = useReducer((state, action) => { const { payload, name } = action; // 根據(jù)不同的action類型來更新state switch (name) { case 'add': return state + 1; case 'sub': return state - 1; case 'reset': return payload; default: return state; } }, 0); return ( <div> 當(dāng)前值:{number} {/* 派發(fā)更新 */} <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函數(shù),這個函數(shù)接受當(dāng)前的state和一個action作為參數(shù),并返回新的state。如果新的state和之前的state指向的是同一個內(nèi)存地址,那么組件就不會更新。
useSyncExternalStore的出現(xiàn)與React版本18中更新模式下外部數(shù)據(jù)的撕裂(tearing)密切相關(guān)。它允許React組件在并發(fā)模式下安全有效地讀取外部數(shù)據(jù)源,并在組件渲染過程中檢測數(shù)據(jù)的變化,以及在數(shù)據(jù)源發(fā)生變化時調(diào)度更新,以確保結(jié)果的一致性。
基礎(chǔ)介紹如下:
useSyncExternalStore( subscribe, getSnapshot, getServerSnapshot)
基礎(chǔ)用法示例如下:
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 });/* 創(chuàng)建 store */const store = createStore(rootReducer, { number: 1 });function Index() { /* 訂閱外部數(shù)據(jù)源 */ const state = useSyncExternalStore(store.subscribe, () => store.getState().number); console.log(state); return ( <div> {state} <button onClick={() => store.dispatch({ type: 'ADD' })}>點擊</button> </div> );}
當(dāng)點擊按鈕時,會觸發(fā)reducer,然后會觸發(fā)store.subscribe訂閱函數(shù),執(zhí)行g(shù)etSnapshot得到新的number,判斷number是否發(fā)生變化,如果有變化,則觸發(fā)更新。
在React v18中,引入了一種新概念叫做過渡任務(wù)。這些任務(wù)與立即更新任務(wù)相對應(yīng),通常指的是一些不需要立即響應(yīng)的更新,比如頁面從一個狀態(tài)過渡到另一個狀態(tài)。
舉個例子,當(dāng)用戶點擊tab從tab1切換到tab2時,會產(chǎn)生兩個更新任務(wù):
這兩個任務(wù)中,用戶通常希望hover狀態(tài)的響應(yīng)更迅速,而內(nèi)容的響應(yīng)可能需要更長時間,比如請求數(shù)據(jù)等操作。因此,第一個任務(wù)可以視為立即執(zhí)行任務(wù),而第二個任務(wù)則可以視為過渡任務(wù)。
import { useTransition } from 'react';/* 使用 */const [isPending, startTransition] = useTransition();
useTransition會返回一個數(shù)組,其中包含兩個值:
在基礎(chǔ)用法中,除了切換tab的場景外,還有很多其他場景適合產(chǎn)生過渡任務(wù),比如實時搜索并展示數(shù)據(jù)。這種情況下,有兩個優(yōu)先級的任務(wù):第一個是受控表單的實時響應(yīng),第二個是輸入內(nèi)容改變后數(shù)據(jù)展示的變化。
下面是一個基本使用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') //立即響應(yīng)的任務(wù),立即更新任務(wù) const [ renderData, setRenderData ] = React.useState(tab[active]) //不需要立即響應(yīng)的任務(wù),過渡任務(wù) const [ isPending,startTransition ] = React.useTransition() const handleChangeTab = (activeItem) => { setActive(activeItem) //立即更新 startTransition(()=>{ //startTransition里面的任務(wù)優(yōu)先級低 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>}
以上示例中,當(dāng)切換tab時,會產(chǎn)生兩個優(yōu)先級任務(wù):第一個任務(wù)是setActive控制tab的active狀態(tài)的改變,第二個任務(wù)是setRenderData控制渲染的長列表數(shù)據(jù)(在實際場景中,這可能是一些數(shù)據(jù)量大的可視化圖表)。
在React 18中,引入了useDeferredValue,它可以讓狀態(tài)的更新滯后于派生。useDeferredValue的實現(xiàn)效果類似于transition,在緊急任務(wù)執(zhí)行后,再得到新的狀態(tài),這個新的狀態(tài)就稱之為DeferredValue。
useDeferredValue和前面提到的useTransition有什么異同呢?
相同點: useDeferredValue和useTransition本質(zhì)上都是標(biāo)記為過渡更新任務(wù)。
不同點: useTransition將內(nèi)部的更新任務(wù)轉(zhuǎn)換為過渡任務(wù)transition,而useDeferredValue則是通過過渡任務(wù)得到新的值,這個值作為延遲狀態(tài)。一個是處理一段邏輯,另一個是生成一個新的狀態(tài)。
useDeferredValue接受一個參數(shù)value,通常是可變的state,返回一個延遲狀態(tài)deferredValue。
const deferredValue = React.useDeferredValue(value)
下面將上面的例子改用useDeferredValue來實現(xiàn)。
export default function Index(){ const [ active, setActive ] = React.useState('tab1') //需要立即響應(yīng)的任務(wù),立即更新任務(wù) const deferredActive = React.useDeferredValue(active) // 將狀態(tài)延遲更新,類似于過渡任務(wù) const handleChangeTab = (activeItem) => { setActive(activeItem) // 立即更新 } const renderData = tab[deferredActive] // 使用延遲狀態(tài) 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是正常改變的狀態(tài),deferredActive是延遲的active狀態(tài)。我們使用正常狀態(tài)來改變tab的active狀態(tài),而使用延遲狀態(tài)來更新視圖,從而提升了用戶體驗。
React hooks提供了API,用于彌補函數(shù)組件沒有生命周期的不足。主要利用了hooks中的useEffect、useLayoutEffect和useInsertionEffect。其中,最常用的是useEffect。現(xiàn)在我們來看一下useEffect的使用。
useEffect(() => { return cleanup;}, dependencies);
useEffect的第一個參數(shù)是一個回調(diào)函數(shù),返回一個清理函數(shù)cleanup。cleanup函數(shù)會在下一次回調(diào)函數(shù)執(zhí)行之前調(diào)用,用于清除上一次回調(diào)函數(shù)產(chǎn)生的副作用。
第二個參數(shù)是一個依賴項數(shù)組,里面可以包含多個依賴項。當(dāng)依賴項發(fā)生變化時,會執(zhí)行上一次callback返回的cleanup函數(shù),并執(zhí)行新的effect回調(diào)函數(shù)。
對于useEffect的執(zhí)行,React采用了異步調(diào)用的處理邏輯。對于每個effect的回調(diào)函數(shù),React會將其放入任務(wù)隊列中,類似于setTimeout回調(diào)函數(shù)的方式,等待主線程任務(wù)完成、DOM更新、JS執(zhí)行完成以及視圖繪制完成后才執(zhí)行。因此,effect回調(diào)函數(shù)不會阻塞瀏覽器的視圖繪制。
/* 模擬數(shù)據(jù)交互 */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); /* 模擬事件監(jiān)聽處理函數(shù) */ const handleResize = () => {}; /* useEffect使用 */ useEffect(() => { /* 請求數(shù)據(jù) */ getUserInfo(a).then(res => { setUserMessage(res); }); /* 定時器 延時器等 */ const timer = setInterval(() => console.log(666), 1000); /* 操作dom */ console.log(div.current); /* div */ /* 事件監(jiān)聽等 */ window.addEventListener('resize', handleResize); /* 此函數(shù)用于清除副作用 */ return function() { clearInterval(timer); window.removeEventListener('resize', handleResize); }; /* 只有當(dāng)props->a和state->number改變的時候, useEffect副作用函數(shù)重新執(zhí)行,如果此時數(shù)組為空[],證明函數(shù)只有在初始化的時候執(zhí)行一次,相當(dāng)于componentDidMount */ }, [a, number]); return ( <div ref={div}> <span>{userMessage.name}</span> <span>{userMessage.age}</span> <div onClick={() => setNumber(1)}>{number}</div> </div> );};
上述代碼中,在useEffect中做了以下功能:
useLayoutEffect和useEffect的不同之處在于它采用了同步執(zhí)行的方式。那么它和useEffect有什么區(qū)別呢?
① 首先,useLayoutEffect在DOM更新之后、瀏覽器繪制之前執(zhí)行。這使得我們可以方便地修改DOM、獲取DOM信息,從而避免了不必要的瀏覽器回流和重繪。相比之下,如果將DOM布局修改放在useEffect中,那么useEffect的執(zhí)行是在瀏覽器繪制視圖之后進(jìn)行的,接著再去修改DOM,可能會導(dǎo)致瀏覽器進(jìn)行額外的回流和重繪。由于兩次繪制,可能會導(dǎo)致視圖上出現(xiàn)閃現(xiàn)或突兀的效果。
② useLayoutEffect回調(diào)函數(shù)中的代碼執(zhí)行會阻塞瀏覽器的繪制。
const DemoUseLayoutEffect = () => { const target = useRef(); useLayoutEffect(() => { /* 在DOM繪制之前,移動DOM到指定位置 */ const { x, y } = getPositon(); // 獲取要移動的x,y坐標(biāo) animate(target.current, { x, y }); }, []); return ( <div> <span ref={target} className="animate"></span> </div> );};
useInsertionEffect是React v18新增的hooks之一,其用法與useEffect和useLayoutEffect相似。那么這個hooks用于什么呢?
在介紹useInsertionEffect用途之前,先來看一下useInsertionEffect的執(zhí)行時機。
React.useEffect(() => { console.log('useEffect 執(zhí)行');}, []);React.useLayoutEffect(() => { console.log('useLayoutEffect 執(zhí)行');}, []);React.useInsertionEffect(() => { console.log('useInsertionEffect 執(zhí)行');}, []);
打印結(jié)果為:useInsertionEffect執(zhí)行 -> useLayoutEffect執(zhí)行 -> useEffect執(zhí)行。
可以看到,useInsertionEffect的執(zhí)行時機要比useLayoutEffect提前。在useLayoutEffect執(zhí)行時,DOM已經(jīng)更新了,但是在useInsertionEffect執(zhí)行時,DOM還沒有更新。useInsertionEffect主要是解決CSS-in-JS在渲染中注入樣式的性能問題。這個hooks主要適用于這個場景,在其他場景下React不建議使用這個hooks。
export default function Index() { React.useInsertionEffect(() => { /* 動態(tài)創(chuàng)建style標(biāo)簽插入到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的使用。
可以使用useContext來獲取父級組件傳遞過來的context值,這個值是最近的父級組件Provider設(shè)置的value值。useContext的參數(shù)通常是由createContext方式創(chuàng)建的context對象,也可以是父級上下文context傳遞的(參數(shù)為context)。useContext可以代替context.Consumer來獲取Provider中保存的value值。
const contextValue = useContext(context);
useContext接受一個參數(shù),一般是context對象,返回值是context對象內(nèi)部保存的value值。
/* 用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> );}
useRef可以用來獲取元素,緩存狀態(tài)。它接受一個初始狀態(tài)initState作為初始值,并返回一個ref對象cur。cur對象上有一個current屬性,該屬性就是ref對象需要獲取的內(nèi)容。
const cur = React.useRef(initState);console.log(cur.current);
獲取DOM元素: 在React中,可以利用useRef來獲取DOM元素。在React Native中雖然沒有DOM元素,但是同樣可以利用useRef來獲取組件的節(jié)點信息(Fiber信息)。
const DemoUseRef = () => { const dom = useRef(null); const handleSubmit = () => { console.log(dom.current); // <div>表單組件</div> DOM節(jié)點 } return ( <div> {/* ref標(biāo)記當(dāng)前DOM節(jié)點 */} <div ref={dom}>表單組件</div> <button onClick={handleSubmit}>提交</button> </div> );}
保存狀態(tài): 可以利用useRef返回的ref對象來保存狀態(tài),只要當(dāng)前組件不被銷毀,狀態(tài)就會一直存在。
const status = useRef(false);/* 改變狀態(tài) */const handleChangeStatus = () => { status.current = true;}
useImperativeHandle配合forwardRef可以自定義向父組件暴露的實例值。對于函數(shù)組件,如果我們想讓父組件能夠獲取子組件的實例,就可以使用useImperativeHandle和forwardRef來實現(xiàn)。
useImperativeHandle接受三個參數(shù):
我們通過一個示例來說明,使用useImperativeHandle使得父組件能夠控制子組件中的input自動聚焦并設(shè)置值。
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="請輸入內(nèi)容" 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> ); }}
useMemo 可以在函數(shù)組件的渲染過程中同步執(zhí)行一個函數(shù)邏輯,并將其返回值作為一個新的狀態(tài)進(jìn)行緩存。這個 hooks 的作用在于優(yōu)化性能,避免不必要的重復(fù)計算或渲染。
const cachedValue = useMemo(create, deps)
基本用法:
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 派生出一個新的狀態(tài) contextValue,只有 keeper 發(fā)生變化時,才會重新生成 contextValue。
function Scope(){ const style = useMemo(()=>{ let computedStyle = {} // 大量的計算 return computedStyle },[]) return <div style={style} ></div>}
在這個例子中,通過 useMemo 緩存了一個計算結(jié)果 style,只有當(dāng)依賴項發(fā)生變化時,才會重新計算 style。
function Scope ({ children }){ const renderChild = useMemo(()=>{ children() },[ children ]) return <div>{ renderChild } </div>}
通過 useMemo 緩存了子組件的渲染結(jié)果 renderChild,只有當(dāng) children 發(fā)生變化時,才會重新執(zhí)行子組件的渲染。
useCallback 和 useMemo 接收的參數(shù)類似,都是在其依賴項發(fā)生變化后才執(zhí)行,都返回緩存的值。但是它們的區(qū)別在于,useMemo 返回的是函數(shù)運行的結(jié)果,而 useCallback 返回的是一個經(jīng)過處理的函數(shù)本身。它主要用于優(yōu)化性能,避免不必要的函數(shù)重新創(chuàng)建,特別是在向子組件傳遞函數(shù)時,避免因為函數(shù)重新創(chuàng)建而導(dǎo)致子組件不必要的重新渲染。
const cachedCallback = useCallback(callbackFunction, deps)
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]) // 只有當(dāng) id 發(fā)生變化時,才會重新創(chuàng)建 getInfo 函數(shù) return ( <div> <button onClick={ ()=>setNumber(number+1) }>增加</button> <DemoChildren getInfo={getInfo} /> </div> )}
在上面的示例中,getInfo 函數(shù)通過 useCallback 進(jìn)行了緩存,只有當(dāng) id 發(fā)生變化時,才會重新創(chuàng)建 getInfo 函數(shù)。這樣可以避免因為函數(shù)重新創(chuàng)建而導(dǎo)致子組件不必要的重新渲染。
在組件的頂層調(diào)用 useId 生成唯一 ID:
import { useId } from 'react';function PasswordField() { const passwordHintId = useId(); // ...
在你的 自定義 Hook 的頂層調(diào)用 useDebugValue,以顯示可讀的調(diào)試值:
import { useDebugValue } from 'react';function useOnlineStatus() { // ... useDebugValue(isOnline ? 'Online' : 'Offline'); // ...}
本文鏈接:http://www.www897cc.com/showinfo-26-84029-0.htmlReactHooks由淺入深:所有 hooks 的梳理、匯總與解析
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com