React Hooks學習指北

一、前言

在當今的前端開發環境中,越來越多的開發者認可了 Hooks 的強大能力,并紛紛加入到 Hooks 的使用大軍中:

  • 2019 年 2 月,React 正式發布 v16.8 版本,引入 Hooks 能力(最新的 v18 中,還新增了 5 個 Hooks API);
  • 2019 年 6 月,尤雨溪提出了 Vue3 Composition API 的提案,使 Vue3 中也能夠使用 Hooks;
  • 諸如 Ant Design Pro V5 等框架以及 Solid.jsPreact 等庫,都選擇將 Hooks 作為主體;
  • 很多優秀的開源項目(如 Ant Design)已經從原本的 Class 升級到使用 Hooks;

在 React v16.8 之前,我們主要使用 Class 組件,對函數組件的使用相對較少。這主要是因為函數組件雖然簡潔,但由于缺乏數據狀態管理,這一致命的缺陷使得 Class 組件成為主流選擇。

引入 Hooks 后,帶來了一系列優勢:

  • 擺脫了繁瑣的 super 傳遞;
  • 消除了 Class 組件中容易引發奇怪 this 指向的問題;
  • 摒棄了繁雜的生命周期方法。

此外,Hooks 提供了更好的狀態復用。從強化組件模型的角度來看,我們可以發現自定義 Hooks 的模式與 mixin 模式更為相近。

為什么 mixin 會被廢棄呢?其主要原因是 mixin 存在諸多弊端,其中一個顯著的問題是引發了組件之間的耦合性增強。Mixin 模式使得組件之間共享狀態和邏輯,但這也導致了一系列問題,例如:

  1. 命名沖突: 不同組件可能會定義相同名稱的 mixin,從而造成命名沖突,使代碼難以維護和理解。
  2. 復雜性增加: 隨著 mixin 的引入,組件的復雜性呈指數增長。混合了多個 mixin 的組件往往難以追蹤和調試,增加了代碼維護的困難度。
  3. 難以追蹤數據流: 組件的狀態和邏輯被分散在多個 mixin 中,使得數據流難以追蹤和理解。這增加了排查錯誤和進行性能優化的難度。
  4. 組件間耦合: 由于 mixin 的引入,組件之間的耦合性增強。一個組件可能會依賴于其他組件中定義的 mixin,導致組件之間的依賴關系錯綜復雜。
  5. 繼承鏈問題: mixin 使用繼承鏈來將邏輯注入到組件中,但這會導致不可預測的繼承鏈問題,特別是在復雜的項目中。

總體而言,mixin 的弊端主要表現在引入了難以管理的復雜性、命名沖突、耦合性增強等方面,因此 React 官方明確表示不建議使用 mixin,而推薦采用更靈活、可維護的 Hooks 模式。Hooks 提供了更清晰、可組合的方式來處理組件的狀態和邏輯,避免了 mixin 帶來的諸多問題。

React 官方在提供 Hooks API 后,并沒有強制要求開發者立刻轉向使用它,而是通過明確 Hooks 的優勢與劣勢,讓開發者自行選擇。這種漸進的改變讓項目中的開發者可以同時使用熟悉的 Class 組件和嘗試新穎的 Hooks。隨著項目的逐步迭代,開發者在實踐中逐漸體會到 Hooks 的優勢。這種悄無聲息的變革使越來越多的開發者熟悉并紛紛加入 Hooks 的行列。

二、實戰演練

主要演示v16提供的10種和v18中提供的5種 React Hooks API的使用

1. useState

useState: 定義變量,使其具備類組件的 state,讓函數式組件擁有更新視圖的能力。

基本使用:

const [state, setState] = useState(initData)

Params:

  • initData:默認初始值,有兩種情況:函數和非函數,如果是函數,則函數的返回值作為初始值。

Result:

  • state:數據源,用于渲染UI 層的數據源;
  • setState:改變數據源的函數,可以理解為類組件的 this.setState

案例:

主要介紹兩種setState的使用方法。

import { useState } from "react";
import { Button } from "antd";const Index = () => {const [count, setCount] = useState(0);return (<><div>數字:{count}</div><Button type="primary" onClick={() => setCount(count + 1)}>第一種方式+1</Button><Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => setCount((v) => v + 1)}>第二種方式+1</Button></>);
};export default Index;

注意: useState 有點類似于 PureComponent,它會進行一個比較淺的比較,這就導致了一個問題,如果是對象直接傳入的時候,并不會實時更新,這點一定要切記。

我們做個簡單的對比,比如:

import { useState } from "react";
import { Button } from "antd";const Index = () => {const [state, setState] = useState({ number: 0 });const [count, setCount] = useState(0);return (<><div>數字形式:{count}</div><Buttontype="primary"onClick={() => {setCount(count+1);}}>點擊+1</Button><div>對象形式:{state.number}</div><Buttontype="primary"onClick={() => {state.number++;setState(state);}}>點擊+1</Button></>);
};export default Index;

2. useEffect

useEffect: 副作用,這個鉤子成功彌補了函數式組件沒有生命周期的缺陷,是我們最常用的鉤子之一。

基本使用:

useEffect(()=>{ return destory
}, deps)

Params:

  • callback:useEffect 的第一個入參,最終返回 destory,它會在下一次 callback 執行之前調用,其作用是清除上次的 callback 產生的副作用;
  • deps:依賴項,可選參數,是一個數組,可以有多個依賴項,通過依賴去改變,執行上一次的 callback 返回的 destory 和新的 effect 第一個參數 callback。

案例:

模擬掛載和卸載階段

事實上,destory 會用在組件卸載階段上,把它當作組件卸載時執行的方法就 ok,通常用于監聽 addEventListenerremoveEventListener 上,如:

import { useState, useEffect } from "react";
import { Button } from "antd";const Child = () => {useEffect(() => {console.log("掛載");return () => {console.log("卸載");};}, []);return <div>react hooks!</div>;
};const Index = () => {const [flag, setFlag] = useState(false);return (<><Buttontype="primary"onClick={() => {setFlag((v) => !v);}}>{flag ? "卸載" : "掛載"}</Button>{flag && <Child />}</>);
};export default Index;

依賴變化:

dep的個數決定callback什么時候執行,如:

import { useState, useEffect } from "react";
import { Button } from "antd";const Index = () => {const [number, setNumber] = useState(0);const [count, setCount] = useState(0);useEffect(() => {console.log("count改變才會執行");}, [count]);return (<><div>number: {number} count: {count}</div><Button type="primary" onClick={() => setNumber((v) => v + 1)}>number + 1</Button><Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => setCount((v) => v + 1)}>count + 1</Button></>);
};export default Index;

無限執行:

當 useEffect 的第二個參數 deps 不存在時,會無限執行。更加準確地說,只要數據源發生變化(不限于自身中),該函數都會執行,所以請不要這么做,否則會出現不可控的現象。

import { useState, useEffect } from "react";
import { Button } from "antd";const Index = () => {const [count, setCount] = useState(0);const [flag, setFlag] = useState(false);useEffect(() => {console.log("hello hooks!");});return (<><Button type="primary" onClick={() => setCount((v) => v + 1)}>數字加一:{count}</Button><Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => setFlag((v) => !v)}>狀態切換:{JSON.stringify(flag)}</Button></>);
};export default Index;

3. useContext

useContext: 上下文,類似于 Context,其本意就是設置全局共享數據,使所有組件可跨層級實現共享。

useContext 的參數一般是由 createContext 創建,或者是父級上下文 context傳遞的,通過 CountContext.Provider 包裹的組件,才能通過 useContext 獲取對應的值。我們可以簡單理解為 useContext 代替 context.Consumer 來獲取 Provider 中保存的 value 值。

基本使用:

const contextValue = useContext(context)

Params:

  • context:一般而言保存的是 context 對象。

Result:

  • contextValue:返回的數據,也就是context對象內保存的value值。

案例:

子組件 Child 和孫組件 Son,共享父組件 Index 的數據 count。

import { useState, createContext, useContext } from "react";
import { Button } from "antd";const CountContext = createContext(-1);const Index = () => {const [count, setCount] = useState(0);return (<><div>父組件中的count:{count}</div><Button type="primary" onClick={() => setCount((v) => v + 1)}>點擊+1</Button><CountContext.Provider value={count}><Child /></CountContext.Provider></>);
};const Child = () => {const countChild = useContext(CountContext);return (<div style={{ marginTop: 10 }}>子組件獲取到的count: {countChild}<Son /></div>);
};const Son = () => {const countSon = useContext(CountContext);return <div style={{ marginTop: 10 }}>孫組件獲取到的count: {countSon}</div>;
};export default Index;

4. useReducer

useReducer: 功能類似于 redux,與 redux 最大的不同點在于它是單個組件的狀態管理,組件通訊還是要通過 props。簡單地說,useReducer 相當于是 useState 的升級版,用來處理復雜的 state 變化。

基本使用:

const [state, dispatch] = useReducer((state, action) => {}, initialArg,init
);

Params:

  • reducer:函數,可以理解為 redux 中的 reducer,最終返回的值就是新的數據源 state;
  • initialArg:初始默認值;
  • init:惰性初始化,可選值。

Result:

  • state:更新之后的數據源;
  • dispatch:用于派發更新的dispatchAction,可以認為是useState中的setState

問:什么是惰性初始化?

答:惰性初始化是一種延遲創建對象的手段,直到被需要的第一時間才去創建,這樣做可以將用于計算 state 的邏輯提取到 reducer 外部,這也為將來對重置 state 的 action 做處理提供了便利。換句話說,如果有 init,就會取代 initialArg

案例:

import { useReducer } from "react";
import { Button } from "antd";const Index = () => {const [count, dispatch] = useReducer((state, action) => {switch (action?.type) {case "add":return state + action?.payload;case "sub":return state - action?.payload;default:return state;}}, 0);return (<><div>count:{count}</div><Buttontype="primary"onClick={() => dispatch({ type: "add", payload: 1 })}>1</Button><Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => dispatch({ type: "sub", payload: 1 })}>1</Button></>);
};export default Index;

特別注意: 在 reducer 中,如果返回的 state 和之前的 state 值相同,那么組件將不會更新。

比如這個組件是子組件,并不是組件本身,然后我們對上面的例子稍加更改,看看這個問題:

const Index = () => {console.log("父組件發生更新");...return (<>...<Buttontype="primary"style={{ marginLeft: 10 }}onClick={() => dispatch({ type: "no", payload: 1 })}>無關按鈕</Button><Child count={count} /></>)
};const Child = ({ count }) => {console.log("子組件發生更新");return <div>在子組件的count:{count}</div>;
};

可以看到,當 count 無變化時,子組件并不會更新。

5. useMemo

場景: 在每一次的狀態更新中,都會讓組件重新繪制,而重新繪制必然會帶來不必要的性能開銷,為了防止沒有意義的性能開銷,React Hooks 提供了 useMemo 函數。

useMemo:理念與 memo 相同,都是判斷是否滿足當前的限定條件來決定是否執行callback 函數。它之所以能帶來提升,是因為在依賴不變的情況下,會返回相同的引用,避免子組件進行無意義的重復渲染。

基本使用:

const cacheData = useMemo(fn, deps)

Params:

  • fn:函數,函數的返回值會作為緩存值;
  • deps:依賴項,數組,會通過數組里的值來判斷是否進行 fn 的調用,如果發生了改變,則會得到新的緩存值。

Result:

  • cacheData:更新之后的數據源,即 fn 函數的返回值,如果 deps 中的依賴值發生改變,將重新執行 fn,否則取上一次的緩存值。

案例:

import { useState } from "react";
import { Button } from "antd";const usePow = (list) => {return list.map((item) => {console.log("我是usePow");return Math.pow(item, 2);});
};const Index = () => {const [flag, setFlag] = useState(true);const data = usePow([1, 2, 3]);return (<><div>數字集合:{JSON.stringify(data)}</div><Button type="primary" onClick={() => setFlag((v) => !v)}>狀態切換{JSON.stringify(flag)}</Button></>);
};export default Index;

從例子中來看, 按鈕切換的 flag 應該與 usePow 的數據毫無關系,

可以看到,當我們點擊按鈕后,會打印我是usePow,這樣就會產生開銷。毫無疑問,這種開銷并不是我們想要見到的結果,所以有了 useMemo。 并用它進行如下改造:

const usePow = (list) => {return useMemo(() =>list.map((item) => {console.log(1);return Math.pow(item, 2);}),[]);
};

6. useCallback

useCallback:與 useMemo 極其類似,甚至可以說一模一樣,唯一不同的點在于,useMemo 返回的是值,而 useCallback 返回的是函數。

基本使用:

const resfn = useCallback(fn, deps)

Params:

  • fn:函數,函數的返回值會作為緩存值;
  • deps:依賴項,數組,會通過數組里的值來判斷是否進行 fn 的調用,如果依賴項發生改變,則會得到新的緩存值。

Result:

  • resfn:更新之后的數據源,即 fn 函數,如果 deps 中的依賴值發生改變,將重新執行 fn,否則取上一次的函數。

案例:

import { useState, useCallback, memo } from "react";
import { Button } from "antd";const Index = () => {let [count, setCount] = useState(0);let [flag, setFlag] = useState(true);const add = useCallback(() => {setCount(count + 1);}, [count]);return (<><TestButton onClick={() => setCount((v) => v + 1)}>普通點擊</TestButton><TestButton onClick={add}>useCallback點擊</TestButton><div>數字:{count}</div><Button type="primary" onClick={() => setFlag((v) => !v)}>切換{JSON.stringify(flag)}</Button></>);
};const TestButton = memo(({ children, onClick = () => {} }) => {console.log(children);return (<Buttontype="primary"onClick={onClick}style={children === "useCallback點擊" ? { marginLeft: 10 } : undefined}>{children}</Button>);
});export default Index;

簡要說明下,TestButton 里是個按鈕,分別存放著有無 useCallback 包裹的函數,在父組件 Index 中有一個 flag 變量,這個變量同樣與 count 無關,那么,我們切換按鈕的時候,TestButton 會怎樣執行呢?

可以看到,我們切換 flag 的時候,沒有經過 useCallback 的函數會再次執行,而包裹的函數并沒有執行(點擊“普通點擊”按鈕的時候,useCallbak 的依賴項 count 發生了改變,所以會打印出 useCallback 點擊)。

7. useRef

useRef: 用于獲取當前元素的所有屬性,除此之外,還有一個高級用法:緩存數據。

基本使用:

const ref = useRef(initialValue);

Params:

  • initialValue:初始值,默認值。

Result:

  • ref:返回的一個 current 對象,這個 current 屬性就是 ref 對象需要獲取的內容。

案例:

import { useState, useRef } from "react";const Index = () => {const scrollRef = useRef(null);const [clientHeight, setClientHeight] = useState(0);const [scrollTop, setScrollTop] = useState(0);const [scrollHeight, setScrollHeight] = useState(0);const onScroll = () => {if (scrollRef?.current) {let clientHeight = scrollRef?.current.clientHeight; //可視區域高度let scrollTop = scrollRef?.current.scrollTop; //滾動條滾動高度let scrollHeight = scrollRef?.current.scrollHeight; //滾動內容高度setClientHeight(clientHeight);setScrollTop(scrollTop);setScrollHeight(scrollHeight);}};return (<><div><p>可視區域高度:{clientHeight}</p><p>滾動條滾動高度:{scrollTop}</p><p>滾動內容高度:{scrollHeight}</p></div><divstyle={{ height: 200, border: "1px solid #000", overflowY: "auto" }}ref={scrollRef}onScroll={onScroll}><div style={{ height: 2000 }}></div></div></>);
};export default Index;

8. useImperativeHandle

useImperativeHandle:可以通過 ref 或 forwardRef 暴露給父組件的實例值,所謂的實例值是指值和函數。

實際上這個鉤子非常有用,簡單來講,這個鉤子可以讓不同的模塊關聯起來,讓父組件調用子組件的方法。

舉個例子,在一個頁面很復雜的時候,我們會將這個頁面進行模塊化,這樣會分成很多個模塊,有的時候我們需要在最外層的組件上控制其他組件的方法,希望最外層的點擊事件同時執行子組件的事件,這時就需要 useImperativeHandle 的幫助(在不用redux等狀態管理的情況下)。

基本使用:

useImperativeHandle(ref, createHandle, deps)

Params:

  • ref:接受 useRef 或 forwardRef 傳遞過來的 ref;
  • createHandle:處理函數,返回值作為暴露給父組件的 ref 對象;
  • deps:依賴項,依賴項如果更改,會形成新的 ref 對象。

案例:

父組件是函數式組件:

import { useState, useRef, useImperativeHandle } from "react";
import { Button } from "antd";const Child = ({cRef}) => {const [count, setCount] = useState(0)useImperativeHandle(cRef, () => ({add}))const add = () => {setCount((v) => v + 1)}return <div><p>點擊次數:{count}</p><Button onClick={() => add()}> 子組件的按鈕,點擊+1</Button></div>
}const Index = () => {const ref = useRef<any>(null)return (<><div>hello hooks!</div><div></div><Buttontype="primary"onClick={() =>  ref.current.add()}>父組件上的按鈕,點擊+1</Button><Child cRef={ref} /></>);
};export default Index;

當父組件是類組件時:

如果當前的父組件是 Class 組件,此時不能使用 useRef,而是需要用 forwardRef 來協助我們處理。

forwardRef:引用傳遞,是一種通過組件向子組件自動傳遞引用 ref 的技術。對于應用者的大多數組件來說沒什么作用,但對于一些重復使用的組件,可能有用。

經過 forwardRef 包裹后,會將 props(其余參數)和 ref 拆分出來,ref 會作為第二個參數進行傳遞。如:

import { useState, useRef, useImperativeHandle, Component, forwardRef } from "react";
import { Button } from "antd";const Child = (props, ref) => {const [count, setCount] = useState(0)useImperativeHandle(ref, () => ({add}))const add = () => {setCount((v) => v + 1)}return <div><p>點擊次數:{count}</p><Button onClick={() => add()}> 子組件的按鈕,點擊+1</Button></div>
}const ForwardChild = forwardRef(Child)class Index extends Component{countRef = nullrender(){return   <><div>hello hooks!</div><div></div><Buttontype="primary"onClick={() => this.countRef.add()}>父組件上的按鈕,點擊+1</Button><ForwardChild ref={node => this.countRef = node} /></>}
}export default Index;

9. useLayoutEffect

useLayoutEffect: 與 useEffect 基本一致,不同點在于它是同步執行的。簡要說明:

  • 執行順序:useLayoutEffect 是在 DOM 更新之后,瀏覽器繪制之前的操作,這樣可以更加方便地修改 DOM,獲取 DOM 信息,這樣瀏覽器只會繪制一次,所以 useLayoutEffect 的執行順序在 useEffect 之前;
  • useLayoutEffect 相當于有一層防抖效果;
  • useLayoutEffect 的 callback 中會阻塞瀏覽器繪制。

基本使用:

useLayoutEffect(callback,deps)

案例:

防抖效果:

import { useState, useEffect, useLayoutEffect } from "react";const Index = () => {const [count, setCount] = useState(0);const [count1, setCount1] = useState(0);useEffect(() => {if(count === 0){setCount(10 + Math.random() * 100)}}, [count])useLayoutEffect(() => {if(count1 === 0){setCount1(10 + Math.random() * 100)}}, [count1])return (<><div>hello Hooks!</div><div>useEffect的count:{count}</div><div>useLayoutEffect的count:{count1}</div></>);
};export default Index;

在這個例子中,我們分別設置 count 和 count1 兩個變量,初始值都為 0,然后分別通過 useEffect 和 useLayout 控制,通過隨機值來變更兩個變量的值。也就是說,count 和 count1 連續變更了兩次。

從結果上來看,count 要比 count1 更加抖動。

這是因為兩者的執行順序,簡要分析下:

  • useEffect 執行順序:setCount 設置 => 在 DOM 上渲染 => useEffect 回調 => setCount 設置 => 在 DOM 上渲染。
  • useLayoutEffect 執行順序:setCount 設置 => useLayoutEffect 回調 => setCount 設置 => 在 DOM 上渲染。

可以看出,useEffect 實際進行了兩次渲染,這樣就可能導致瀏覽器再次回流和重繪,增加了性能上的損耗,從而會有閃爍突兀的感覺。

10. useDebugValue

useDebugValue: 可用于在 React 開發者工具中顯示自定義 Hook 的標簽。這個 Hooks 目的就是檢查自定義 Hooks。

注意: 這個標簽并不推薦向每個 hook 都添加 debug 值。當它作為共享庫的一部分時才最有價值。(也就是自定義 Hooks 被復用的值)。因為在一些情況下,格式化值可能是一項開銷很大的操作,除非你需要檢查 Hook,否則沒有必要這么做。

基本使用:

useDebugValue(value, (status) => {})

Params:

  • value:判斷的值;
  • callback:可選,這個函數只有在 Hook 被檢查時才會調用,它接受 debug 值作為參數,并且會返回一個格式化的顯示值。

案例:

function useFriendStatus(friendID) {const [isOnline, setIsOnline] = useState(null);// ...// 在開發者工具中的這個 Hook 旁邊顯示標簽  // e.g. "FriendStatus: Online"  useDebugValue(isOnline ? 'Online' : 'Offline');return isOnline;
}

11. useSyncExternalStore

useSyncExternalStore: 會通過強制的同步狀態更新,使得外部 store 可以支持并發讀取。

注意: 這個 Hooks 并不是在日常開發中使用的,而是給第三方庫 reduxmobx 使用的,因為在 React v18 中,主推的 Concurrent(并發)模式可能會出現狀態不一致的問題(比如在 react-redux 7.2.6 的版本),所以官方給出 useSyncExternalStore 來解決此類問題。

簡單地說,useSyncExternalStore 能夠讓 React 組件在 Concurrent 模式下安全、有效地讀取外接數據源,在組件渲染過程中能夠檢測到變化,并且在數據源發生變化的時候,能夠調度更新。

當讀取到外部狀態的變化,會觸發強制更新,以此來保證結果的一致性。

基本使用:

const state = useSyncExternalStore(subscribe,getSnapshot,getServerSnapshot
)

Params:

  • subscribe:訂閱函數,用于注冊一個回調函數,當存儲值發生更改時被調用。 此外,useSyncExternalStore 會通過帶有記憶性的 getSnapshot 來判斷數據是否發生變化,如果發生變化,那么會強制更新數據;
  • getSnapshot:返回當前存儲值的函數。必須返回緩存的值。如果 getSnapshot 連續多次調用,則必須返回相同的確切值,除非中間有存儲值更新;
  • getServerSnapshot:返回服務端(hydration 模式下)渲染期間使用的存儲值的函數。

Result:

  • state:數據源,用于渲染 UI 層的數據源。

案例:

import { useSyncExternalStore } from "react";
import { Button } from "antd";
import { combineReducers, createStore } from "redux";const reducer = (state = 1, action) => {switch (action.type) {case "ADD":return state + 1;case "DEL":return state - 1;default:return state;}
};/* 注冊reducer,并創建store */
const rootReducer = combineReducers({ count: reducer });
const store = createStore(rootReducer, { count: 1 });const Index = () => {//訂閱const state = useSyncExternalStore(store.subscribe,() => store.getState().count);return (<><div>Hooks!</div><div>數據源: {state}</div><Button type="primary" onClick={() => store.dispatch({ type: "ADD" })}>1</Button><Buttonstyle={{ marginLeft: 8 }}onClick={() => store.dispatch({ type: "DEL" })}>1</Button></>);
};export default Index;

當我們點擊按鈕后,會觸發 store.subscribe(訂閱函數),執行 getSnapshot 后得到新的 count,此時 count 發生變化,就會觸發更新。

12. useTransition

useTransition: 返回一個狀態值表示過渡更新任務的等待狀態,以及一個啟動該過渡更新任務的函數。

問:什么是過渡更新任務?

答:過渡任務是對比緊急更新任務所產生的。

緊急更新任務指,輸入框、按鈕等任務需要在視圖上立即做出響應,讓用戶立馬能夠看到效果的任務。

但有時,更新任務不一定那么緊急,或者說需要去請求數據,導致新的狀態不能夠立馬更新,需要一個 loading... 的狀態,這類任務稱為過渡任務。

我們再來舉個比較常見的例子幫助理解緊急更新任務和過渡更新任務。

當我們有一個 input 輸入框,這個輸入框的值要維護一個很大列表(假設列表有 1w 條數據),比如說過濾、搜索等情況,這時有兩種變化:

  1. input 框內的變化;
  2. 根據 input 的值,1w 條數據的變化。

input 框內的變化是實時獲取的,也就是受控的,此時的行為就是緊急更新任務。而這 1w 條數據的變化,就會有過濾、重新渲染的情況,此時這種行為被稱為過渡更新任務。

基本使用:

const [isPending, startTransition] = useTransition();

Result:

  • isPending:布爾值,過渡狀態的標志,為 true 時表示等待狀態;
  • startTransition:可以將里面的任務變成過渡更新任務。

案例:

import { useState, useTransition } from "react";
import { Input } from "antd";const Index = () => {const [isPending, startTransition] = useTransition();const [input, setInput] = useState("");const [list, setList] = useState([]);return (<><div>Hooks!</div><Inputvalue={input}onChange={(e) => {setInput(e.target.value);startTransition(() => {const res = [];for (let i = 0; i < 10000; i++) {res.push(e.target.value);}setList(res);});}}/>{isPending ? (<div>加載中...</div>) : (list.map((item, index) => <div key={index}>{item}</div>))}</>);
};export default Index;

從上述的代碼可以看到,我們通過 input 去維護了 1w 條數據,通過 isPending 的狀態來控制是否展示完成。

13. useDeferredValue

useDeferredValue:可以讓狀態滯后派生,與 useTransition 功能類似,推遲屏幕優先級不高的部分。

在一些場景中,渲染比較消耗性能,比如輸入框。輸入框的內容去調取后端服務,當用戶連續輸入的時候會不斷地調取后端服務,其實很多的片段信息是無用的,這樣會浪費服務資源, React 的響應式更新和 JS 單線程的特性也會導致其他渲染任務的卡頓。而 useDeferredValue 就是用來解決這個問題的。

問:useDeferredValue 和 useTransition 怎么這么相似,兩者有什么異同點?

答:useDeferredValue 和 useTransition 從本質上都是標記成了過渡更新任務,不同點在于 useDeferredValue 是將原值通過過渡任務得到新的值, 而 useTransition 是將緊急更新任務變為過渡任務。

也就是說,useDeferredValue 用來處理數據本身,useTransition 用來處理更新函數。

基本使用:

const deferredValue = useDeferredValue(value);

Params:

  • value:接受一個可變的值,如useState所創建的值。

Result:

  • deferredValue:返回一個延遲狀態的值。

案例:

import { useState, useDeferredValue } from "react";
import { Input } from "antd";const getList = (key) => {const arr = [];for (let i = 0; i < 10000; i++) {if (String(i).includes(key)) {arr.push(<li key={i}>{i}</li>);}}return arr;
};const Index = () => {//訂閱const [input, setInput] = useState("");const deferredValue = useDeferredValue(input);console.log("value:", input);console.log("deferredValue:", deferredValue);return (<><div>Hooks!</div><Input value={input} onChange={(e) => setInput(e.target.value)} /><div><ul>{deferredValue ? getList(deferredValue)}</ul></div></>);
};export default Index;

上述的功能類似于搜索,從 1w 個數中找到輸入框內的數。

問:什么場景下使用useDeferredValueuseTransition

答:通過上面的兩個例子介紹我們知道,useDeferredValue 和 useTransition 實際上都是用來處理數據量大的數據,比如,百度輸入框、散點圖等,都可以使用。它們并不適用于少量數據。

但在這里更加推薦使用 useTransition,因為 useTransition 的性能要高于 useDeferredValue,除非像一些第三方的 Hooks 庫,里面沒有暴露出更新的函數,而是直接返回值,這種情況下才去考慮使用 useDeferredValue。

這兩者可以說是一把雙刃劍,在數據量大的時候使用會優化性能,而數據量低的時候反而會影響性能。

14. useInsertionEffect

useInsertionEffect: 與 useEffect 一樣,但它在所有 DOM 突變之前同步觸發。

注意:

  • useInsertionEffect 應限于 css-in-js 庫作者使用。在實際的項目中優先考慮使用 useEffect 或 useLayoutEffect 來替代;
  • 這個鉤子是為了解決 CSS-in-JS 在渲染中注入樣式的性能問題而出現的,所以在我們日常的開發中并不會用到這個鉤子,但我們要知道如何去使用它。

基本使用:

useInsertionEffect(callback,deps)

案例:

import { useInsertionEffect } from "react";const Index = () => {useInsertionEffect(() => {const style = document.createElement("style");style.innerHTML = `.css-in-js{color: blue;}`;document.head.appendChild(style);}, []);return (<div><div className="css-in-js">,一起學Hooks吧!</div></div>);
};export default Index;

執行順序: 在目前的版本中,React 官方共提供三種有關副作用的鉤子,分別是 useEffect、useLayoutEffect 和 useInsertionEffect,我們一起來看看三者的執行順序:

import { useEffect, useLayoutEffect, useInsertionEffect } from "react";const Index = () => {useEffect(() => console.log("useEffect"), []);useLayoutEffect(() => console.log("useLayoutEffect"), []);useInsertionEffect(() => console.log("useInsertionEffect"), []);return <div>,Hooks!</div>;
};export default Index;

從效果上來看,可知三者的執行的順序為:useInsertionEffect > useLayoutEffect > useEffect。

15. useId

useId: 是一個用于生成橫跨服務端和客戶端的穩定的唯一 ID ,用于解決服務端與客戶端產生 ID 不一致的問題,更重要的是保證了 React v18 的 streaming renderer (流式渲染)中 id 的穩定性。

這里我們簡單介紹一下什么是 streaming renderer

在之前的 React ssr 中,hydrate( 與 render 相同,但作用于 ReactDOMServer 渲染的容器中 )是整個渲染的,也就是說,無論當前模塊有多大,都會一次性渲染,無法局部渲染。但這樣就會有一個問題,如果這個模塊過于龐大,請求數據量大,耗費時間長,這種效果并不是我們想要看到的。

于是在 React v18 上誕生出了 streaming renderer (流式渲染),也就是將整個模塊進行拆分,讓加載快的小模塊先進行渲染,大的模塊掛起,再逐步加載出大模塊,就可以就解決上面的問題。

此時就有可能出現:服務端和客戶端注冊組件的順序不一致的問題,所以 useId 就是為了解決此問題而誕生的,這樣就保證了 streaming renderer 中 ID 的穩定性。

基本使用:

const id = useId();

Result:

  • id:生成一個服務端和客戶端統一的id

案例:

import { useId } from "react";const Index = () => {const id = useId();return <div id={id}>一起學Hooks吧!</div>;
};export default Index;

三、自定義hooks

什么是自定義hooks

自定義hooks是在react-hooks基礎上的一個拓展,可以根據業務需要制定滿足業務需要的hooks,更注重的是邏輯單元。通過業務場景不同,我們到底需要react-hooks做什么,怎么樣把一段邏輯封裝起來,做到復用,這是自定義hooks產生的初衷。

如何設計一個自定義hooks,設計規范

邏輯 + 組件

hooks 專注的就是邏輯復用, 我們的項目,不僅僅停留在組件復用的層面上。hooks讓我們可以將一段通用的邏輯存封起來。將我們需要它的時候,開箱即用即可。

1.驅動條件

hooks本質上是一個函數。函數的執行,決定于無狀態組件自身的執行上下文。每次函數的執行(本質上就是組件的更新)就是執行自定義hooks的執行,由此可見組件本身執行和hooks的執行如出一轍。

那么prop的修改,useState,useReducer使用是無狀態組件更新條件,那么就是驅動hooks執行的條件。

2.通用模式

我們設計的自定義react-hooks應該是長的這樣的。

const [ xxx , ... ] = useXXX(參數A,參數B...)

在我們在編寫自定義hooks的時候,要特別~特別關注的是傳進去什么返回什么。 返回的東西是我們真正需要的。更像一個工廠,把原材料加工,最后返回我們。

3.條件限定

如果自定義hooks沒有設計好,比如返回一個改變state的函數,但是沒有加條件限定,就有可能造成不必要的上下文的執行,更有甚的是導致組件的循環渲染執行。

比如:我們寫一個非常簡單hooks來格式化數組將小寫轉成大寫

import React , { useState } from 'react'
/* 自定義hooks 用于格式化數組將小寫轉成大寫 */
function useFormatList(list){return list.map(item=>{return item.toUpperCase()})
}
/* 父組件傳過來的list = [ 'aaa' , 'bbb' , 'ccc'  ] */
function index({ list }){const [ number ,setNumber ] = useState(0)const newList = useFormatList(list)return <div><div className="list" >{ newList.map(item=><div key={item} >{ item }</div>) }</div><div className="number" ><div>{ number }</div><button onClick={()=> setNumber(number + 1) } >add</button></div></div>
}
export default index

上述問題,我們格式化父組件傳遞過來的list數組,并將小寫變成大寫,但是當我們點擊add。 理想狀態下數組不需要重新format,但是實際跟著執行format。無疑增加了性能開銷。

所以我們在設置自定義hooks的時候,一定要把條件限定-性能開銷加進去。

于是乎我們這樣處理一下。

function useFormatList(list) {return useMemo(() => list.map(item => {return item.toUpperCase()}), [])
}

所以一個好用的自定義hooks,一定要配合useMemo, useCallbackapi一起使用。

第三方hooks庫推薦:

ahooks 是由螞蟻 umi 團隊、淘系 ice 團隊以及阿里體育團隊共同建設的 React Hooks 工具庫。ahooks 基于 React Hooks 的邏輯封裝能力,提供了大量常見好用的 Hooks,可以極大降低代碼復雜度,提升開發效率。

四、小結

hook名稱功能
useState定義變量,使其具備類組件的state,讓函數組件更新視圖的能力
useEffect副作用,這個鉤子成功彌補了函數式組件沒有生命周期的缺陷
useContext上下文,類似于Context,其本意就是設置全局共享數據,使所有組件可跨層級實現共享
useReducer功能類似于redux,于redux最大的不同點是在于它是單個組件的狀態管理,組件通訊還是要通過props,是一種useState的升級版,處理復雜的state變化
useMemo理念與memo相同,都是判斷是否滿足當前的限定條件來決定是否執行callback函數
useCallback與useMemo類似,甚至可以說一模一樣,唯一不同的點在于,useMemo返回的是值,而useCallback返回的函數
useRef用于獲取當前元素的所有屬性,除此之外,還有一個高級用法:緩存數據
useImperativeHandle可以通過ref或forwardRef暴露給父組件的實例值,所謂的實例值是指值和函數
useLayoutEffect與useEffect基本一致,不同點在于它是同步執行的
useDebugValue可用于在React開發者工具中顯示自定義hook的標簽。這個hooks目的就是檢查自定義hooks
useSyncExternalStore會通過強制的同步狀態更新,使得外部store可以支持并發讀取
useTransition返回一個狀態值表示過渡更新任務的等待狀態,以及一個啟動該過渡更新任務的函數
useDeferredValue可以讓狀態滯后派生,與useTransition 類似,允許用戶推遲屏幕更新優先級不分高低
useInsertionEffect與useEffect一樣,但它在所有DOM突變之前同步觸發
useId一個用于生成橫跨服務端和客戶端的穩定的唯一ID,用于解決了服務端與客戶端產生ID不一致的問題

參考鏈接:

  • https://mp.weixin.qq.com/s/TovRZ-SsUaeLplCVSvhnpg
  • https://juejin.cn/post/6890738145671938062?searchId=20231102205318349AE1907161FD35C254
  • https://juejin.cn/post/6944863057000529933?searchId=20231102211152FB4D52BD86A7BA3B6CDE
  • https://juejin.cn/post/7236158655128125498

轉轉研發中心及業界小伙伴們的技術學習交流平臺,定期分享一線的實戰經驗及業界前沿的技術話題。

關注公眾號「轉轉技術」(綜合性)、「大轉轉FE」(專注于FE)、「轉轉QA」(專注于QA),更多干貨實踐,歡迎交流分享~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/215654.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/215654.shtml
英文地址,請注明出處:http://en.pswp.cn/news/215654.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

移液器吸頭材質選擇——PFA吸頭在半導體化工行業的應用

PFA吸頭是一種高性能移液器配件&#xff0c;這種材料具有優異的耐化學品、耐熱和電絕緣性能&#xff0c;使得PFA吸頭在應用中表現出色。那么它有哪些特點呢&#xff1f; 首先&#xff0c;PFA吸頭具有卓越的耐化學腐蝕性能。無論是酸性溶液、堿性溶液還是有機溶劑&#xff0c;P…

如何用CHAT幫你提高工作效率?

問CHAT&#xff1a;從規范項目管理流程交付&#xff0c;分別對項目信息安全管理&#xff0c;項目預算管理和項目采購管理三個方面提建議 CHAT回復&#xff1a; 項目信息安全管理: 1. 制定詳細的信息安全政策&#xff0c;所有參與項目的員工必須遵守&#xff0c;對其中涉及敏感…

wpf TelerikUI使用DragDropManager

首先&#xff0c;我先創建事務對象ApplicationInfo&#xff0c;當暴露出一對屬性當例子集合對于構成ListBoxes。這個類在例子中顯示如下代碼&#xff1a; public class ApplicationInfo { public Double Price { get; set; } public String IconPath { get; set; } public …

亞馬遜S3V4驗簽與MINIO驗簽區別

1、先看下官方文檔 AWS S3V4 DEMO 2、實際調用試試 1&#xff09;代碼 // 計算auth// for a simple GET, we have no body so supply the precomputed empty hashMap<String, String> headers new HashMap<String, String>();headers.put("x-amz-content…

0013Java安卓程序設計-ssm酒品移動電商平臺app

文章目錄 **摘要**目錄系統實現5.1 APP端5.2管理員功能模塊開發環境 編程技術交流、源碼分享、模板分享、網課分享 企鵝&#x1f427;裙&#xff1a;776871563 摘要 首先,論文一開始便是清楚的論述了系統的研究內容。其次,剖析系統需求分析,弄明白“做什么”,分析包括業務分析…

Firewalld 防火墻配置

文章目錄 Firewalld 防火墻配置1. Firewalld 概述2. 區域名稱及策略規則3. Firewalld 配置方法4. Firewalld 參數和命令5. Firewalld 兩種模式6. Firewalld 使用 Firewalld 防火墻配置 1. Firewalld 概述 firewalld 是一個動態防火墻管理器&#xff0c;作為 Systemd 管理的防…

【docker】常用命令

啟動docker服務 systemctl start docker 停止docker服務 systemctl stop docker 重啟docker服務 systemctl restart docker 查看docker服務狀態 systemctl status docker 設置開機啟動docker服務 systemctl enable docker 設置關閉開機啟動docker服務 systemctl disable …

數據在內存中的存儲(浮點型篇)

1.例子&#xff1a;5.5&#xff1a;內存存儲為101.1&#xff0c;十分位百分位依次為2的-1次方&#xff0c;2的-2次方&#xff0c;而使用科學計數法可以改寫為1.011*2的2次方 2.國際標準公式&#xff1a;-1的D次方*M*2的E次方&#xff0c;x1負0正 3.M在存儲時默認整數部分為1&…

使用Spring Boot和領域驅動設計實現模塊化整體

用模塊化整體架構編寫的代碼實際上是什么樣的&#xff1f;借助 Spring Boot 和 DDD&#xff0c;我們踏上了編寫可維護和可演化代碼的旅程。 當談論模塊化整體代碼時&#xff0c;我們的目標是以下幾點&#xff1a; 應用程序被組織成模塊。每個模塊解決業務問題的不同部分。模塊…

springcloud微服務篇--1.認識微服務

一、服務架構演變。 單體架構&#xff1a; 將業務的所有功能集中在一個項目中開發&#xff0c;打成一個包部署。 優點&#xff1a;架構簡單 &#xff0c;部署成本低。 缺點&#xff1a;耦合度高 分布式架構 根據業務功能對系統進行拆分&#xff0c;每個業務模塊作為獨立項…

[idea]idea連接clickhouse23.6.2.18

一、安裝驅動 直接在pom.xml加上那個lz4也是必要的不然會報錯 <dependency><groupId>com.clickhouse</groupId><artifactId>clickhouse-jdbc</artifactId><version>0.4.2</version></dependency><dependency><group…

歌唱比賽計分 (8 分)設有10名歌手(編號為1-10)參加歌詠比賽

未采用結構體的解法&#xff0c;通過二維數組解題 #include <stdio.h> void rank(int arr[10][6] ) { int str[4] { 0 }; int a1[6] { 0 }; int k 0; int i 0; int z 0; int j 0; int temp 0; double s1[10][2] { 0 }; dou…

(1)mysql容器化部署

mysql容器化部署&#xff1a; 數據持久化&#xff08;方便數據保存及遷移&#xff09;: 需要持久化兩個目錄: 創建/mysql (1)mysql配置文件: /mysql/mysql-cnf/my.cnf vim my.cnf [mysqld] pid-file /var/run/mysqld/mysqld.pid socket /var/run/mysqld/…

【51單片機系列】使用74HC595控制數碼管顯示

使用74HC595結合數碼管顯示字符。 proteus仿真設計如下&#xff0c;74HC595的輸出端連接到動態數碼管的位選和靜態數碼管的段選&#xff0c;動態數碼管的段選連接到P0口。這兩個數碼管都是共陰極的。 靜態數碼管顯示字符0-F&#xff0c;軟件設計如下&#xff1a; /*實現功能&a…

Java:SpringBoot獲取當前運行的環境activeProfile

代碼示例 /*** 啟動監聽器*/ Component public class AppListener implements ApplicationListener<ApplicationReadyEvent> {Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// 獲取當前的環境&#xff0c;如果是test&#xff0c;則直接返回Co…

redis實際應用實現合集

一、redis實現搶紅包的功能&#xff08;set 數據結構&#xff09; 分兩種情況&#xff1a; 情況一: 從10個觀眾中隨機抽2名幸運觀眾 首先需要把10個觀眾的id&#xff08;具體是什么id可以根據實際業務情況自己定義&#xff09;放到redis 的 set 集合里 然后隨機抽取2名幸運…

【hcie-cloud】【8】華為云Stack_LLD設計【部署設計、資源設計、服務設計、學習推薦、縮略語】【下】

設計概覽、整體架構設計、網絡設計 看下面-這篇文章 【hcie-cloud】【7】華為云Stack_LLD設計【設計概覽、整體架構設計、網絡設計、部署設計、資源設計、服務設計】【上】 部署設計 云平臺整體部署架構 圖中在Region下每個灰底都代表一個數據中心&#xff0c;AZ1可以跨數據…

yarn系統架構與安裝

1.1 YARN系統架構 YARN的基本思想是將資源管理和作業調度/監視功能劃分為單獨的守護進程。其思想是擁有一個全局ResourceManager (RM)&#xff0c;以及每個應用程序擁有一個ApplicationMaster (AM)。應用程序可以是單個作業&#xff0c;也可以是一組作業。 一個ResourceManage…

ai智能機器人外呼系統怎么操作?

什么是ai智能機器人外呼&#xff1f;ai智能機器人外呼怎么操作&#xff1f;當下&#xff0c;很多企業主已經認識到&#xff0c;AI外呼是一種高效的拉新引流手段。但具體到實際應用中&#xff0c;實現的效果好像并沒有那么理想。從企業外呼的結果來看&#xff0c;接通率是可以達…