詳解如何構建自己的react hooks
1. 常用的一個 hooks
官方中提供了幾個內建的鉤子,我們簡單瞭解下他們的用法。
1.1 useState: 狀態鉤子
需要更新頁面狀態的資料,我們可以把他放到 useState 的鉤子裡。例如點選按鈕一下,資料加 1 的操作:
const [count,setCount] = useState(0); return (<> <p>{ count}</p> <button onClick = { () => setCount(count + 1) }> add 1 </button> </> );
在 typescript 的體系中,count 的型別,預設就是當前初始值的型別,例如上面例子中的變數就是 number
型別。如果我們想自定義這個變數的型別,可以在 useState 後面進行定義:
const [count,setCount] = useState程式設計客棧<number | null>(null); // 變數count為number型別或者null型別
同時,使用 useState 改變狀態時,是整個把 state 替換掉的,因此,若狀態變數是個 object 型別的資料,我只想修改其中的某個欄位,在之前 class 元件內呼叫 setState 時,他內部會自動合併資料。
class Home extends React.Component { state = { name: 'wenzi',age: 20,score: 89 }; update() { this.setState({ score: 98 }); // 內部自動合併 } }
但在 function 元件內使用 useState 時,需要自己先合併資料,然後再呼叫方法,否則會造成欄位的丟失。
const [person,setPerson] = useState({ name: 'wenzi',score: 89 }); setPerson({ ...person,{ score: 98 } }); // 先合併資料 { name: 'wenzi',score: 98 } setPerson({ score: 98 }); // 僅傳入要修改的欄位,後name和age欄位丟失
1.2 useEffect: 副作用鉤子
useEffect 可以看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函式的組合。
useEffect 鉤子在元件初始化完畢時,一定會執行一次,在元件重新渲染的過程中,是否還要 update,還要看傳入的第 2 個引數。
- 當只有回撥函式這一個引數時,元件的每次更新,回撥都會執行;
- 當有 2 個引數時,只有第 2 引數裡的資料發生變化時,回撥才執行;
- 只想在元件初始化完畢時只執行一次,第 2 個引數可以傳入一個空的陣列;
我們可以看下這個例子,無論點選 add按鈕 還是 settime按鈕 ,useEffect 的回撥都會執行:
const Home = () => { const [count,setCount] = useState(0); const [nowtime,setNowtime] = useState(0); useEffect(() => { console.log('count',count); console.log('nowtime',nowtime); }); return ( <> <p>count: {count} </p> <p>nowtime: {nowtime} </p> <button onClick = {() => setCount(count + 1)}> add 1 </button> <button onClick = {() => setNowtime(Date.now())} > set now time </button> </>); };
若改成下面的這樣,回撥僅會在 count 發生變化時才會在控制檯輸出,僅修改 nowtime 的值時沒有輸出:
useEffect(() => { console.log('count',count); console.log('nowtime',nowtime); },[count]);
useEffect 的回撥函式還可以返回一個函式,這個函式在 effect 生命週期結束之前呼叫。為防止記憶體洩漏,清除函式會在元件解除安裝前執行。另外,如果元件多次渲染,則在執行下一個 effect 之前,上一個 effect 就已被清除。
基於上面的程式碼,我們稍微修改一下:
useEffect(() => { console.log('count'www.cppcns.com,coun程式設計客棧t); console.log('nowtime',nowtime); return () => console.log('effect callback will be cleared'); },[count]);
基於這個機制,在一些存在新增繫結和取消繫結的案例上特別合適,例如監聽頁面的視窗大小變化、設定定時器、與後端的 websocket 介面建立MhVVRDQ連線和斷開連線等,都可以預計 useEffect MhVVRDQ進行二次的封裝,形成自定義的 hook。關於自定義 hook,下面我們會講到。
1.3 useMemo 和 useCallback
function 元件中定義的變數和方法,在元件重新渲染時,都會重新重新進行計算,例如下面的這個例子:
const Home = () => { const [count,setNowtime] = useState(0); const getSum = () => { const sum = ((1 + count) * count) / 2; return sum + ',' + Math.random(); // 這個random是為了看到區別 }; return ( <> <p> count: {count}< /p> <p> sum: {getSum()}</p> <p> nowtime: {nowtime}</p> <button onClick = {() => setCount(count + 1)} > add 1 </button> <button onClick = {() => setNowtime(Date.now())}> set now time </button> </>); };
這裡有 2 個按鈕,一個是 count+1,一個設定當前的時間戳, getSun() 方法是計算從 1 到 count 的和,我們每次點選 add 按鈕後,sum 方法都會重新計算和。可是當我們點選 settime 按鈕時,getSum 方法也會重新計算,這是沒有必要的。
這裡我們可以使用 useMemo 來修改下:
const sum = useMemo(() => ((1 + count) * count) / 2 + ',' + Math.random(),[count]); <p> {sum} </p>;
修改後就可以看到,sum 的值只有在 count 發生變化的時候才重新計算,當點選 settime 按鈕的時候,sum 並沒有重新計算。這要得益於 useMemo 鉤子的特性:
const memoizedValue = useMemo(() => computeExpensiveValue(a,b),[a,b]);
useMemo 返回回撥裡 return 的值,而且 memoizedValue 它僅會在某個依賴項改變時才重新計算。這種優化有助於避免在每次渲染時都進行高開銷的計算。如果沒有提供依賴項陣列,useMemo 在每次渲染時都會計算新的值。
在上面的例子裡,只有 count 變數發生變化時,才重新計算 sum,否則 sum 的值保持不變。
useCallback 與 useMemo 型別,只不過 useCallback 返回的是一個函式,例如:
const fn = useCallback(() => { return ((1 + count) * count) / 2 + ',' + nowtime; },[count]);
2. 實現幾個自定義的 hook
在官方文件裡,實現了好友的線上與離線功能。這裡我們自己也學著實現幾個 hook。
2.1 獲取視窗變化的寬高
我們通過監聽resize事件來獲取實時獲取window視窗的寬高,對這個方法進行封裝後可以在生命週期結束前能自動解綁resize事件:
const useWinResize = () => { const [size,setSize] = useState({ width: document.documentElement.clientWidth,height: document.documentElement.clientHeight }); const resize = useCallback(() => { setSize({ width: document.documentElement.clientWidth,height: document.documentElement.clientHeight }) },[]) useEffect(() => { window.addEventListener('resize',resize); return () => window.removeEventListener('resize',resize); },[]); return size; }
使用起來也非常方便:
const Home = () => { const {width,height} = useWinResize(); return <div> <p>width: {width}</p> <p>height: {height}</p> </div>; };
2.2 定時器 useInterval
在前端中使用定時器時,通常要在元件生命週期結束前清除定時器,如果定時器的週期發生變化了,還要先清除定時器再重新按照新的週期來啟動。這種最常用的場景就是九宮格抽獎,使用者點選開始抽獎後,先緩慢啟動,然後逐漸變快,介面返回中獎結果後,再開始減速,最後停止。
我們很容易想到用 useEffect 來實現這樣的一個 hook:
const useInterval = (callback,delay) => { useEffect(() => { if (delay !== null) { let id = setInterval(callback,delay); return () => clearInterval(id); } },[delay]); };
我們把這段程式碼用到專案中試試:
const Home = () => { const [count,setCount] = useState(0); useInterval(() => { console.log(count); setCount(count + 1); },500); return <div > { count } < /div>; };
可是這段執行後很奇怪,頁面從 0 到 1 後,就再也不變了, console.log(count) 的輸出表明程式碼並沒有卡死,那麼問題出在哪兒了?
React 元件中的 props 和 state 是可以改變的, React 會重渲染它們且「丟棄」任何關於上一次渲染的結果,它們之間不再有相關性。
useEffect() Hook 也「丟棄」上一次渲染結果,它會清除上一次 effect 再建立下一個 effect,下一個 effect 鎖住新的 props 和 state,這也是我們第一次嘗試簡單示例可以正確工作的原因。
但 setInterval 不會「丟棄」。 它會一直引用老的 props 和 state 直到你把它換掉 —— 不重置時間你是無法做到的。
這裡就要用到useRef這個 hook 了,我們把 callback 儲存到 ref 中,當 callback 更新時去更新 ref.current
的值:
const useInterval = (callback,delay) => { const saveCallback = useRef(); useEffect(() => { // 每次渲染後,儲存新的回撥到我們的 ref 裡 saveCallback.current = callback; }); useEffect(() => { function tick() { saveCallback.current(); } if (delay !== null) { let id = setInterval(tick,[delay]); };
當我們使用新的 useInterval 時,發現就可以自增了
這裡我們使用一個變數來控制增加的速度:
const [count,setCount] = useState(0); const [diff,setDiff] = useState(500); useInterval(() => { setCount(count + 1); },diff); return ( <div> <p> count: {count} </p> <p> diff: {diff}ms </p> <p> <button onClick = {() => setDiff(diff - 50)}> 加快50ms </button> <button onClick = {() => setDiff(diff + 50)} > 減慢50ms </button> </p> </div>);
分別點選兩個按鈕,可以調整count增加的速度。
3. 總結
使用react hook可以做很多有意思的事情,這裡我們也僅僅是舉幾個簡單的例子,後續我們也會更加深入瞭解hook的原理。
以上就是如何構建自己的 react hooks的詳細內容,更多關於如何構建自己的 react hooks的資料請關注我們其它相關文章!