今年五月份就開始接觸react hook,六月份也在組內分享過一次,但由於太忙所以現在才抽出時間寫這篇關於hook的學習手冊。
前沿:
react hook相信對於前端開發者來說也不會陌生,就是基於react16.8之後推出的解決函數式組件沒辦法處理內部狀態持久化的一種解決方式,它能大大的提高了函數式組件的在react之中的地位。本文主要介紹react hook鉤子的一些使用方式,至於原理性的東西后續補上。
背景:
在介紹react hook之前,還是先介紹一下爲啥會出現react hook,使用它之後的函數式組件與類組件之間相比存在哪些優點。
- class組件存在的問題:
1)多個類組件之間的邏輯較難複用:
如果多個組件之間存在相同的邏輯或者相類似的邏輯,一般會採取兩種方式來進行復用,一種是通過高階組件HOC來處理,另一種則是採用了render prop的方式。
例如一個場景爲A組件是點擊按鈕,然後出現一個彈窗;B組件是文本,鼠標移入後出現一個彈窗;
從上面可以看出handle要被複用,還要再繞一圈通過HOC,而且還要裝飾器,才能複用handle,代碼的複雜度上升且代碼量也多了。
2)複雜組件很難理解:
很多時候類組件一開始比較簡單,而幾個月後組件'變胖'了,render主體越來越多,綁定和調用的方法也越來越多,越來越多生命週期被調用而且生命週期內部的邏輯越來越複雜,有時候如監聽函數的綁定和解除需要拆分成兩部分放在兩個生命週期等,原來組件才一百多行代碼,一下子變成上千行代碼。
3)class組件的生命週期使用時的坑:
相信很多開發者在使用類組件的時候,會經常在調用生命週期時踩過不少的坑,這裏我大概總結了一下生命週期的坑:
- class組件和函數式組件的對比:
如表中所示,因爲函數式組件功能單一,以至於在hooks出來之前,一般不常用函數式組件。
- hooks鉤子能解決什麼:
先對比一下類組件和運用了react hook的函數式組件,從圖中可以清晰的看到類組件該有的東西,函數式組件運用了鉤子之後才擁有了。
再用一個例子簡單的對比一下,表格中列舉了兩個組件,然後分別用普通方式,HOC方式,react hook方式,可以看出代碼的可讀性,簡潔性以及複用性鉤子會更優異一點。
所以簡單的總結了react hook的幾個適用場景:
1)讓對於複雜龐大的組件,可以拆分成顆粒度更小的函數式組件,每個小組件各自控制自身的狀態;
2)將一段業務代碼封裝成複用,提供給每個組件;
常用api:
前面闡述了這麼多,主要時讓大家知道react hook的存在意義以及和類組件之間的對比,接下來要重點介紹一下react hook常用的幾個鉤子。
- useState:
作用:可以讓函數式組件中加入簡單的狀態管理,而且可以使用多次useState,但每個狀態之間是相互獨立,即更新一個狀態,另一個狀態不會隨着也更新,但要注意只能在函數式組件中使用。
語法:
[狀態,狀態更新函數] = useState(狀態默認值)
這裏的狀態和狀態更新函數沒有具體的變量聲明,所以可以採用各種各樣的聲明變量。
注意點:
1)每更新一次狀態,函數式組件就會重新在渲染一次;
2)利用狀態改變函數改變了狀態後,該狀態不會立即同步,還是原來的值,只有rerender時,該狀態纔會最新;
3)狀態改變函數是直接將新的值代替舊的值,而不像setState那樣能局部更新,然後將局部更新的狀態合併到整個狀態之中;
const [apple, setApple] = useState({a: {a1: 1}, b: 2});
setApple({a: {a1: 3}}) // 此時apple的值是{a1: 3},而不會是{a: {a1: 3}, b: 2}
state = {a: {a1: 1}, b: 2};
setState({a: {a1: 3}});
{a: {a1: 3}, b: 2}
useState的demo:
function FunCom(props) {
const [apple, setApple] = useState('apple');
useEffect(() => { document.title = `${apple}`; });
return (
<div>
<p onClick={(e) => { setApple('banana'); console.log(apple); }}>
函數式組件-{apple}
</p>
</div>
)
}
- useEffect:
對於react來說,當渲染後(不管是首次渲染還是重複渲染),還會存在其他一些操作:數據獲取,操作dom節點,設置訂閱或發佈等操作;
1) 類組件:一般該副作用會放置在componentDidMount,componentDidUpdate或者componentWillUnMount;
2) 函數式組件:一般副作用放置在鉤子裏面執行;
語法:useEffect(callback);
其中:
1) callback是一個回調函數,它裏面可以訪問到最新的狀態,但不能直接調用同步方式下的更新狀態的函數;
2) useEffect函數在首次和之後的每次渲染後,銷燬前都會調用(理解爲componentDidMount,componentDidUpdate或者componentWillUnMount時刻下的調用)。
3) 該鉤子是異步觸發;
effect調用過程:
性能優化:useEffect(callback, [callback內部的狀態或者屬性])
注意:
- 如果是[ ],則該鉤子只會在mount和unmount發生反應,在update的時候不會發生反應;
- 如果依賴項值不變,useEffect返回的清除函數就不會再下一次渲染中被執行,只有當依賴項的值變化,在渲染的時候,清除函數纔會執行然後再執行useEffect;
- useContext:
語法:const value = React.useContext(Context);
其中Context是指React.createContext對象,value是指該對象所傳遞下去給後代組件的值;
作用:不管函數值組件是不是Context.Provider組件的子孫組件,都可以獲取Context組件將要透傳下去的值;
注意點:
- 該組件可以放在Context.Provider裏面,也可以放在外面,所以與Context.Provider的關係無關;
- 如果Provider組件的值改變,該鉤子也會讓該函數式組件重新渲染;
const Context = React.createContext('hello world');
<Context.Provider value="asd"/>
<Middle/>
</Context.Provider>
<FunCom/>
function Middle() {
return (
<div>
<Bottom/>
<FunCom/>
<div>
)
}
function Bottom(props) {
return <Context.Consumer>
{
val => {
return <div>{value}-jiji</div>
}
}
</Context.Consumer>
}
function FunCom(props) {
const context = useContext(Context);
console.log('context:', context);
return (
<div>
<p>context上下文</p>
</div>
)
}
- useReducer:
語法:const {state, dispatch} = useReducer(reducer, initState);
其中state就是當前的狀態值,dispatch就是用來觸發reducer跟新狀態的方法,reducer是純函數,initState爲state的初始值;
useState和useReducer的區別:
前者狀態更新是的本質是代替,更新前後的數據結構有可能不一樣;而後者則是狀態的合併,類似於setState,將局部狀態合併到當前狀態;對於簡單的數據結構,可以用前者,對於複雜的數據結構,則需要用到後者;
例子:狀態爲{a: {a1: 1}, b: 2},先更新a1爲3
demo:
- useCallback鉤子和useMemo鉤子:
背景:由於函數式組件沒有scu,只要父組件或者內部的狀態發生變化,都會直接讓函數式組件rerender,此時如果函數式組件中存在:
1) 某個值需要依賴某個狀態進行比較複雜的計算和循環計算等;
2) 含有子組件,並且子組件的屬性指向某個狀態;
此時不管該狀態有沒有發生變化,都會進行重新的渲染和計算,從而造成資源的浪費;
function A() {
const [a1, setA1] = useState(10);
const [a2, setA2] = useState(20);
const value = () => {
let sum = 0;
for(let i = 0; i < a1; i++) { sum = sum + i; }
return sum;
}
return (
<div>
<p>{value()}</p>
<B cb={value}/>
<button onClick={() => { setA2(21); }}/>
</div>
)
作用:
useCallback和useMemo這兩個鉤子,主要是作用緩存作用,只要輸入源的值沒變,他們就會直接採用前一次計算好的緩存,減少沒必要的計算和重複渲染,一旦輸入源的值發生變化,他們就會重新計算,拋棄緩存;這樣做可以優化了組件的性能。
鉤子說明:
1.useMemo鉤子:
語法:const value = useMemo(fn, input);
其中,fn就是含有計算相關的回調函數,input就是輸入源,value就是fn計算出來的值;
說明:input就是fn當中的依賴項,可以是屬性或者內部狀態,一開始時會執行fn,並將值返回給value,當二次渲染時,此時如果input當中的依賴項的值沒有發生變化,則會採用上次執行的結果返回給value,如果input的值發生變化,則會重新執行fn,得到新的value值;
使用場景:如果A組件中有個值需要依賴a狀態進行復雜的計算後得到,並且A組件中的子組件B某個屬性又和該值產生聯繫,此時可以採用useMemo,這樣可以減少無畏的計算和沒必要的渲染。
function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) { sum += i; }
return sum;
}, [count]);
return <div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)}/>
<B val={expensive}/>
</div>
<div>
}
2.useCallback鉤子:
語法:const fn2 = useCallback(fn1, input),
其中fn1是回調函數,input是輸入源,fn2是返回的函數;
說明:首次渲染時,會將fn1返回給fn2,然後執行fn2獲取對應的值;當二次渲染時,input的值沒有發生變化,則fn2還是上一次的fn2,此時fn2雖然還是函數,但前一次和後一次都是指向同一個指針,因此是同一個函數,若input的值發生變化,此時會返回一個新的fn1給fn2,這時候的fn2就會和上一次的fn2不一樣,雖然函數的內容是一樣,但指向不一樣。
使用場景:多數使用在當該函數作爲子組件的屬性函數傳到子組件。
demo:
useEffect,useMemo,useCallback的區別:
useEffect(fn, input)主要是針對副作用,返回的是一個清除函數,而且沒有緩存作用,只有在組件更完成新且input的值發生變化時纔會觸發,他只能減少沒必要的副作用執行。
useMemo,useCallback主要是針對複雜的計算以及子組件的優化(配合子組件的scu),減少沒必要複雜計算和子組件的渲染。
- useRef:
語法:const curRef = useRef(initState);
說明:要區別組件的ref,因爲useRef返回的對象更像一個容器,容器的形式爲{current: initState};該容器可以儲存很多東西,其中包括類組件(與組件的ref綁定),標籤,變量等等,而且該容器會一直存在該函數式組件的整個生命週期,即只有在組件被銷燬的時候才消失;
使用場合:
- 用來獲取類組件,原生組件,標籤的內部元素,只要和他們的ref綁定,就可以在函數式組件中直接訪問(其實就是存儲了一份目標的dom結構);
- 由於它的返回值存在組件整個生命週期,所以可以用來存儲一些變量或者函數(例如定時器返回值等);
注意:
- 不能存儲函數式組件。
- 修改curRef.current的值不會引起組件的重新渲染;
function ShowRef(props) {
const [a, setA] = useState(1);
const mount = useRef({isMount: false});
const dom = useRef(null);
useEffect(() => {
if (!mount.current.isMount) {
console.log('again');
mount.current.isMount = true;
}
console.log('Mount?', mount.current.isMount);
console.log('dom', dom.current);
});
const click = function(e) {
setA(a + 1);
dom.current.style.color = 'blue';
};
return (
<div ref={dom}>
<div onClick={click}>ref1</div>
</div>
)
- useImperativeMethods:
語法:useImperativeMethods(ref, createInstance, [input]);
說明:就是讓父組件能訪問函數式組件內部成分,相當於函數式組件的ref;其中ref是函數式組件傳過來的參數,createInstance是給父組件訪問的屬性和方法實例;
使用場合:就是父組件訪問子組件(函數式組件)的時候用;
demo:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeMethods(ref, () => ({
focus: () => { inputRef.current.focus(); }
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
父組件:<FancyInput ref = {fancyInputRef} />
調用: fancyInputRef.current.focus()
- useLayoutEffect:
語法:useLayoutEffect(fn)
作用:主要使用在dom新更後,用來訪問dom節點佈局方面的操作,例如寬高等;這個是用在處理DOM的時候,當你的useEffect裏面的操作需要處理DOM,並且會改變頁面的樣式,就需要用這個,否則可能會出現出現閃屏問題, useLayoutEffect裏面的callback函數會在DOM更新完成後立即執行,但是會在瀏覽器進行任何繪製之前運行完成,阻塞了瀏覽器的繪製。
以上就是我對react hook使用上的一些理解和總結,如有不對請指出。