簡介
本文主要針對React `16.8.x`提供的`hooks`使用加以介紹,更高版本的中的`hooks`暫無介紹
優勢
- 代碼量少(最直觀的體現)
- 相較於
類組件
使用HOC
、render props
,hooks
更爲簡單方便的複用狀態組件——狀態處理邏輯複用(後面詳細介紹
- 100%向後兼容,與
類組件
可同時使用 - 不需要考慮
this
相關的問題 - 相較於
類
,函數
更容易被機器理解 - 相較於通過生命週期分割代碼,不相干的邏輯放在同一個生命週期函數中,通過功能區分,放在不同的函數中,代碼更容易理解和維護
劣勢
- 使用不當性能問題可能會比
類組件
要嚴重 - 不能使用
decorator
(裝飾器)
官方API
useState
這個hooks應該是用的最多的了,
useState
可接受一個參數爲,任意類型數值
或者 可以返回任意類型數值的函數
,
const [count, setCount] = useState(initialCount);
const [count, setCount] = useState(() => {
//do something
return resultCount
})
然後返回一個數組,數組第一個值爲當前組件最新的 state
,只能通過數組的第二個值來更新,第二個值爲更新 state
的工具函數(該工具函數可以接受一個 值
作爲參數,爲更新後 state
的結果;也可以接受一個 函數
作爲參數,函數的參數爲 state
的前一個值,如果返回值與當前 state
相同則不會重新渲染組件)
function Demo({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
{count}
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
);
}
tip
類組件
可以直接對this.state.something賦值(雖然不會更新視圖),但在hooks 組件
中不能直接對state
賦值hooks 組件
中state
的更新函數同類組件
中setState
一樣是異步的,想要立即獲取更新後的狀態,可以使用useRef
useState
不會同類組件
中setState
一樣是合併更新,而是直接覆蓋更新useState
是基於useReducer
的,建議:簡單的數據結構,不同的狀態放在不同的useState
中,複雜的數據結構直接使用useReducer
。useState
參數爲函數
時,函數只會在初始化是執行一次
const formate = (data) => {
// do some complicated things
return result;
}
const [state, setState] = useState(formate(props.data)); // bad; formate每次渲染都要執行
const [state, setState] = useState(() => formate(props.data)); // good
useReducer
const [state, dispatch] = useReducer(reducer, initialState, initialAction);
useReducer
使用方式類似 redux
,適用於複雜狀態的存儲,同步更新狀態,以及深層次更新組件狀態。支持三個參數,第一個參數爲 reducer
函數,第二個參數爲初始狀態initialState
, 第三參數爲一個函數(可選),用於對 initialState
初始化處理
const reducerFun = (state, action) => {
switch (action.type) {
case 'add':
return {count: state.count + 1}
case 'reduce':
return {count: state.count + 1}
default:
return state
}
}
const initialState = {count: 0};
const initialAction = (init) => {
return {
count: init.count + 1
}
}
function Demo() {
const [state, dispatch] = useReducer(reducerFun, initialState, initialAction)
return (
<div>
{state.count}
<button onClick={() => dispatch({type: 'add'})}>
+
</button>
<button onClick={() => dispatch({type: 'reduce'})}>
-
</button>
</div>
)
}
tip
- 深層次更新組件狀態,可以將
dispatch
作爲props
傳給子組件用於狀態更新 - 使用
useState
獲取的setState
方法更新數據時是異步的;而使用useReducer
獲取的dispatch
方法更新數據是同步的。
useEffect
useEffect(didUpdate, deps);
useEffect
有支持兩個參數,第一個參數爲 effect
(副作用)函數,每次render之後執行,,這個函數可以有返回值,倘若有返回值,返回值也必須是一個函數,姑且稱它爲 清除函數
,會在組件被銷燬時執行(這句話是片面的)。其實不單是在組件銷燬時執行,當組件更新時,清除函數
的函數的執行時機會被放置到,新一次組件 render
之後執行,然後再執行 effect
函數中非 清除函數部分
。
如以下demo:
function Demo() {
const [count, setCount] = useState(0)
const [random, setRandom] = useState(0)
return (
<div>
{count % 2 == 0 && <Child count={count} random={random}/>}
<button onClick={() => setCount(s => s+1)}> + </button>
<button onClick={() => setRandom(Math.random())}>random</button>
</div>
)
}
function Child(props) {
useEffect(() => {
console.log('mounted')
return () => {console.log('unmount')}
})
console.log('render')
return (<h1>{props.count} {props.random}</h1>)
}
可以看出當 Child
組件銷燬時,執行了 清除函數
,Child
組件創建時,先 render
然後執行了 effect
函數。但是更新 Child
組件時先 render
然後執行了 清除函數
,然後纔是 effect
函數
第二個參數可選,爲一個數組,組件重新渲染之後,當數組中有值發生了改變時便會執行副作用函數,否則不執行。
function Demo() {
const [count, setCount] = useState(0)
const [count1, setCount1] = useState(0)
useEffect(() => {
console.log('count') // 只有 count發生改變時纔會打印,count1改變不會
}, [count])
return (
<div>
{count}
<button onClick={() => setCount((s) => s + 1)}>
count+
</button>
<button onClick={() => setCount1((s) => s + 1)}>
count1+
</button>
</div>
)
}
如果想要只在組件初始掛載或者卸載時執行副作用只需將第二個參數置爲 []
function Demo() {
useEffect(() => {
console.log('只有初始掛載或者卸載時執行')
}, [])
return (
<div> </div>
)
}
tip
effect
函數的執行時機是在組件創建或更新render
之後,如果想要在render
時同步觸發副作用可以使用useLayoutEffect
- 不同於
class組件
在各生命週期中的各種副作用的處理,建議將不同的副作用放置不同的useEffect
中通過功能進行區分,便於閱讀理解
useLayoutEffect
useLayoutEffect
的使用形式和 useEffect
是相同的,它們之間的唯一區別是: useLayoutEffect
會在所有 DOM
變更之後同步調用。於是,可以使用它來讀取 DOM
佈局並同步觸發重渲染。在瀏覽器執行繪製之前 useLayoutEffect
內部的更新計劃將被同步刷新。與 componentDidMount
、componentDidUpdate
的調用時機是一樣的。因爲是同步調用,可能會產生視覺更新阻塞的問題,所以儘可能使用標準的 useEffect
以避免阻塞視覺更新
tip
- 執行
DOM
更新操作時useLayoutEffect
會比useEffect
更適合使用 - 涉及使用逐幀動畫
requestAnimationFrame
時,注意執行時機:useLayoutEffect > requestAnimationFrame > useEffect
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(), deps);
useMemo
和 useEffect
兩者語法同樣是一致的,兩者區別是 useEffect
執行的是副作用,一定是在渲染後執行的,useMemo
是需要返回值的,返回值直接參與渲染,因此 useMemo
是在渲染時執行的
先舉個反例:
function Demo(props) {
const [count, setCount] = useState(1);
const calculate = (count) => {
let num = null;
// num = 炒雞複雜的處理邏輯
return num;
}
return (
<div count={calculate(count)}>
</div>
)
}
Demo
組件每次渲染的時候都會執行calculate
這個炒雞複雜的計算過程,而參數count
偏偏沒有改變,每次重複的計算,是不是很浪費?這時候就可以使用useMemo
來優化了。
如下代碼,
function Demo(props) {
const [count, setCount] = useState(1);
const number = useMemo(() => {
let num = null;
// num = 超級複雜的處理邏輯
return num;
}, [count])
return (
<div count={number}>
</div>
)
}
只有在count
發生了改變纔會重新來執行這個炒雞複雜的計算,沒有改變時就直接拿過來之前的計算結果來用,是不是很方便?
tip
- 注意
deps
參數的設置,避免更新錯誤 - 傳入
useMemo
的函數會在渲染期間執行。請不要在函數內部執行與渲染無關的操作,諸如副作用這類的操作屬於useEffect
的適用範疇,而不是useMemo
useMemo
的另一常規用法就是和Rect.memo
搭配使用減少子組件重複渲染,後文會有詳細介紹
useCallback
const memoizedCallback = useCallback(() => {/*do something*/}, deps);
useCallback
用法和用途與 useMemo
類似,同樣支持兩個參數,第一參數爲要緩存的函數,第二個爲判斷是否更新的依賴數組,其主要區別在於 useCallback
返回值爲函數只能用於緩存函數,useMemo
可以用於緩存值和函數。可用於緩存事件回調函數。
同樣先舉個反例:
function Demo(props) {
const clickHandle = () => {
let num = null;
// num = 超級複雜的處理邏輯
return num;
}
return (
<div onClick={clickHandle}>
</div>
)
}
每次組件更新的時候 clickHandle
都需要重新定義,顯然是沒有必要的,因此可以引入useCallback
:
function Demo(props) {
const clickHandle = useCallback(() => {
let num = null;
// num = 超級複雜的處理邏輯
return num;
}, [])
return (
<div onClick={clickHandle}>
</div>
)
}
tip
- 我們可以認爲:
useCallback(fn, deps)
等同於useMemo(() => fn, deps)
useCallback
的另一常規用法就是和Rect.memo
搭配使用減少子組件重複渲染,後文會有詳細介紹
useRef
const refContainer = useRef(initialValue);
useRef
返回一個可變的 ref
對象,其 .current
屬性被初始化爲傳遞的參數(initialValue
)。
function Demo(props) {
const box = useRef(null);
return (
<div onClick={clickHandle} ref={box}>
</div>
)
}
不同於類組件
中的ref
,它不僅僅適用了DOM
元素和類組件
的引用,在hooks 組件
中可以使用其保存任何值,且在組件的整個生命週期內保持不變。可以直接修改.current
屬性的值且不觸發組件更新。
function Demo(props) {
const num = useRef(0);
const [evenNum, setEvenNum] = useState(0)
const add = () => {
num.current += 1;
if(!(num.current & 1)) {
setEvenNum(num.current)
}
}
return (
<h1 onClick={add}>
{evenNum}
</h1>
)
}
如圖,當單次數點擊時數字不會更新,偶次數點擊時數字更新
useImperativeHandle
useImperativeHandle(ref, () => ({}))
在類組件
中想要在父組件中想要獲取子組件實例,只需通過ref
屬性直接獲取,然後就可以調用子組件屬性
或方法
,而useImperativeHandle
就提供了在hooks 組件
中實現該功能的方法;
function Demo() {
const child = useRef(null);
return (
<div>
<Child ref={child}/>
<button onClick={() => {child.current.add()}}>add</button>
</div>
)
}
function Child(props, ref) {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
add: () => setCount((s) => s + 1)
}))
return (
<h1>
{count}
</h1>
)
}
Child = React.forwardRef(Child)
useContext
const value = useContext(MyContext);
useContext
接收一個 context
對象(React.createContext
的返回值)並返回該 context
的當前值,能夠在hooks 組件
中讀取 context
的值以及訂閱 context
的變化。
const MyContext = React.createContext('test')
function Demo() {
const [count, setCount] = useState(0)
return (
<div>
<MyContext.Provider value={count}>
<Child/>
</MyContext.Provider>
<button onClick={() => setCount(s => s + 1)}>+</button>
</div>
)
}
function Child(props) {
const myContextValue = useContext(MyContext)
return (
<h1>
{myContextValue}
</h1>
)
}
tip
- 當組件上層最近的
<MyContext.Provider>
更新時,該Hook
會觸發重渲染,並使用最新傳遞給MyContext provider
的context value
值。即使祖先使用React.memo
或shouldComponentUpdate
,也會在組件本身使用useContext
時重新渲染。 useContext(MyContext)
相當於static contextType = MyContext
在類中,或者<MyContext.Consumer>
。- 搭配
useReducer
可以實現簡易版redux
memo
memo
不屬於 hooks
, 卻是爲 hooks 組件
量身定做的一個 API
,是一個 HOC
,只能用於 函數組件
不能用於 類組件
function MyComponent(props) {}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 傳入 render 方法的返回結果與
將 prevProps 傳入 render 方法的返回結果一致則返回 true,
否則返回 false
*/
}
export default React.memo(MyComponent, areEqual);
類似 類組件
的 React.PureComponent
和 shouldComponentUpdate()
,對傳入組件的 props
進行淺對比或者自定義對比,來決定組件是否需要重新渲染,已達到性能優化的目的。
首先看一個demo:
const MyContext = React.createContext('test')
function Demo() {
const [count, setCount] = useState(0);
const [contextNum, setContextNum] = useState(0);
const [childNum, setChildNum] = useState(0);
const addChildNum = () => {
setChildNum(s => s + 1)
}
const formateChildNum = (n) => ({value: n + 's'});
return (
<div>
<h1>
fatherNum: {count}
</h1>
<MyContext.Provider value={contextNum}>
<Child childNum={formateChildNum(childNum)} addChildNum={addChildNum}/>
</MyContext.Provider>
<button onClick={() => setCount(s => s + 1)}>add fatherNum</button>
<button onClick={() => setContextNum(s => s + 1)}>add contextNum</button>
</div>
)
}
function Child({childNum, addChildNum}) {
const myContextNum = useContext(MyContext)
console.log('render Child')
return (
<div>
<h1>
myContextNum: {myContextNum}
</h1>
<h1>
childNum: {childNum.value}
</h1>
<button onClick={addChildNum}>add childNum</button>
</div>
)
}
可以看出當父組件中fatherNum
改變組件更新時Child
組件也隨之重新渲染了,儘管渲染前後並無變化,這顯然是一次無意義的渲染。這只是一個簡單的demo,父組件只有一個state
改變,子組件只是多了一次渲染,或許無關痛癢,但是如果父組件state
、props
比較多改變比較頻繁,而子組件又十分的‘複雜’,卻額外多餘渲染 N 次,就在用戶體驗上就很可能帶來很差的影響了。
首先我們引入memo
,再次只改變fatherNum
:
+ Child = memo(Child)
function Child({childNum, addChildNum}) {
const myContextNum = useContext(MyContext)
console.log('render Child')
return (
<div>
<h1>
myContextNum: {myContextNum}
</h1>
<h1>
childNum: {childNum.value}
</h1>
<button onClick={addChildNum}>add childNum</button>
</div>
)
}
可以看到,引入 memo
後改變 fatherNum
,父組件重新渲染以後,子組件仍然觸發了無意義的重渲染。和預想的並不一樣。原因在於:雖然傳給 Child
組件 props
的值沒有什麼改變,但是,由於每次hooks 組件
重新渲染以後,組件函數會重新執行一遍,因此 props
中childNum
變成了一個新的object 對象
,同樣 addChildNum
函數也被重新定義了;(內存空間變了)。這時就需要用到 useMemo
和 useCallback
了。
優化後
function Demo() {
const [fatherNum, setFatherNum] = useState(0);
const [contextNum, setContextNum] = useState(0);
const [childNum, setChildNum] = useState(0);
// const addChildNum = () => {
// setChildNum(s => s + 1)
// }
const addChildNum = useCallback(() => {
setChildNum(s => s + 1)
},[]);
// const formateChildNum = (n) => ({value: n + 's'});
const resultChildNum = useMemo(() => ({value: childNum + 's'}), [childNum]);
return (
<div>
<h1>
fatherNum: {fatherNum}
</h1>
<MyContext.Provider value={contextNum}>
<Child childNum={resultChildNum} addChildNum={addChildNum}/>
</MyContext.Provider>
<button onClick={() => setFatherNum(s => s + 1)}>add fatherNum</button>
<button onClick={() => setContextNum(s => s + 1)}>add contextNum</button>
</div>
)
}
自定義hooks
自定義hooks
本質也是函數,不同於一般函數的是,其內部可以使用useState
等API
做狀態管理。正是因爲有了它存在,纔有了hooks 組件
相對於 類組件
最大的優勢 ———— 狀態處理邏輯複用。自定義hooks
的存在允許我們將 UI組件
與 無UI組件
相分離。
栗子
https://github.com/streamich/react-use 一個不錯的自定義hooks庫,除了可以拿來用以外還可以參考源碼,總結自己的經驗也是很不錯的。
tip
自定義hooks
共享複用的是狀態處理邏輯,是邏輯,而不是單純的狀態本身,每hooks
都是獨立的
注意事項
- 不要從常規
JavaScript
函數調用hooks
; - 不要在循環,條件或嵌套函數中調用
hooks
; - 必須在組件的頂層調用
hooks
; - 可以從
React
功能組件調用hooks
; - 可以從自定義
hooks
中調用hooks
; - 自定義
hooks
必須使用use
開頭,這是一種約定; - 建議將不需要
props
或state
的函數提到組件外部。組件每次都會重新渲染,函數每次重新定義,因此一些不必要的函數可以提到函數外面;
最後
本文是對 作者根據自己的經驗以及網上收集到的資料,對 hooks
使用以及注意事項的介紹,可以滿足日常開發使用的需要。如果想要詳細瞭解 其內部的實現機制,與其去看他人消化後的產物,不如直接閱讀源碼:https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.new.js