搞懂這 12 個 Hooks,保證讓你玩轉 React
大家好,我是小杜杜,React Hooks
的發佈已經有三年多了,它給函數式組件帶來了生命週期,現如今,Hooks
逐漸取代class
組件,相信各位 React
開發的小夥伴已經深有體會,然而你真的完全掌握 hooks 了嗎?知道如何去做一個好的自定義 hooks 嗎?
我們知道React Hooks
有useState
設置變量,useEffect
副作用,useRef
來獲取元素的所有屬性,還有useMemo
、useCallback
來做性能優化,當然還有一個自定義Hooks
, 來創造出你所想要的Hooks
接下來我們來看看以下幾個問題,問問自己,是否全都知道:
-
Hooks 的由來是什麼?
-
useRef
的高級用法是什麼? -
useMemo
和useCallback
是怎麼做優化的? -
一個好的自定義 Hooks 該如何設計?
-
如何做一個不需要
useState
就可以直接修改屬性並刷新視圖的自定義 Hooks? -
如何做一個可以監聽任何事件的自定義 Hooks?
如果你對以上問題有疑問,有好奇,那麼這篇文章應該能夠幫助到你~
本文將會以介紹自定義 Hooks 來解答上述問題,並結合 TS,ahooks 中的鉤子,以案列的形式去演示,本文過長,建議:點贊 + 收藏 哦~
注:這裏講解的自定義鉤子可能會和 ahooks
上的略有不同,不會考慮過多的情況,如果用於項目,建議直接使用ahooks
上的鉤子~
如果有小夥伴不懂 TS,可以看看我的這篇文章:一篇讓你完全夠用 TS 的指南 [1]
先附上一張今天的知識圖,還請各位小夥伴多多支持:
深入 Hooks.png
自定義 Hooks 是什麼?
react-hooks
是React16.8
以後新增的鉤子 API,目的是增加代碼的可複用性、邏輯性,最主要的是解決了函數式組件無狀態的問題,這樣既保留了函數式的簡單,又解決了沒有數據管理狀態的缺陷
那麼什麼是自定義 hooks 呢?
自定義hooks
是在react-hooks
基礎上的一個擴展,可以根據業務、需求去制定相應的hooks
, 將常用的邏輯進行封裝,從而具備複用性
如何設計一個自定義 Hooks
hooks
本質上是一個函數,而這個函數主要就是邏輯複用,我們首先要知道一件事,hooks
的驅動條件是什麼?
其實就是props
的修改,useState
、useReducer
的使用是無狀態組件更新的條件,從而驅動自定義 hooks
通用模式
自定義 hooks 的名稱是以 use 開頭,我們設計爲:
const [xxx, ...] = useXXX(參數一,參數二...)
簡單的小例子:usePow
我們先寫一個簡單的小例子來了解下自定義hooks
// usePow.ts
const Index = (list: number[]) => {
return list.map((item:number) => {
console.log(1)
return Math.pow(item, 2)
})
}
export default Index;
// index.tsx
import { Button } from 'antd-mobile';
import React,{ useState } from 'react';
import { usePow } from '@/components';
const Index:React.FC<any> = (props)=> {
const [flag, setFlag] = useState<boolean>(true)
const data = usePow([1, 2, 3])
return (
<div>
<div>數字:{JSON.stringify(data)}</div>
<Button color='primary' onClick={() => {setFlag(v => !v)}}>切換</Button>
<div>切換狀態:{JSON.stringify(flag)}</div>
</div>
);
}
export default Index;
我們簡單的寫了個 usePow
,我們通過 usePow
給所傳入的數字平方, 用切換狀態的按鈕表示函數內部的狀態,我們來看看此時的效果:
我們發現了一個問題,爲什麼點擊切換按鈕也會觸發console.log(1)
呢?
這樣明顯增加了性能開銷,我們的理想狀態肯定不希望做無關的渲染,所以我們做自定義 hooks
的時候一定要注意,需要減少性能開銷, 我們爲組件加入 useMemo
試試:
import { useMemo } from 'react';
const Index = (list: number[]) => {
return useMemo(() => list.map((item:number) => {
console.log(1)
return Math.pow(item, 2)
}), [])
}
export default Index;
發現此時就已經解決了這個問題,所以要非常注意一點,一個好用的自定義hooks
, 一定要配合useMemo
、useCallback
等 Api 一起使用。
玩轉 React Hooks
在上述中我們講了用 useMemo
來處理無關的渲染,接下來我們一起來看看React Hooks
的這些鉤子的妙用(這裏建議先熟知、並使用對應的React Hooks
, 才能造出好的鉤子)
useMemo
當一個父組件中調用了一個子組件的時候,父組件的 state 發生變化,會導致父組件更新,而子組件雖然沒有發生改變,但也會進行更新。
簡單的理解下,當一個頁面內容非常複雜,模塊非常多的時候,函數式組件會從頭更新到尾,只要一處改變,所有的模塊都會進行刷新,這種情況顯然是沒有必要的。
我們理想的狀態是各個模塊只進行自己的更新,不要相互去影響,那麼此時用useMemo
是最佳的解決方案。
這裏要尤其注意一點,只要父組件的狀態更新,無論有沒有對自組件進行操作,子組件都會進行更新,useMemo
就是爲了防止這點而出現的
在講 useMemo
之前,我們先說說memo
,memo
的作用是結合了 pureComponent 純組件和 componentShouldUpdate 功能,會對傳入的 props 進行一次對比,然後根據第二個函數返回值來進一步判斷哪些 props 需要更新。(具體使用會在下文講到~)
useMemo
與memo
的理念上差不多,都是判斷是否滿足當前的限定條件來決定是否執行callback
函數,而useMemo
的第二個參數是一個數組,通過這個數組來判定是否更新回掉函數
這種方式可以運用在元素、組件、上下文中,尤其是利用在數組上,先看一個例子:
useMemo(() => (
<div>
{
list.map((item, index) => (
<p key={index}>
{item.name}
</>
)}
}
</div>
),[list])
從上面我們看出 useMemo
只有在list
發生變化的時候纔會進行渲染,從而減少了不必要的開銷
總結一下useMemo
的好處:
-
可以減少不必要的循環和不必要的渲染
-
可以減少子組件的渲染次數
-
通過特地的依賴進行更新,可以避免很多不必要的開銷,但要注意,有時候在配合
useState
拿不到最新的值,這種情況可以考慮使用useRef
解決
useCallback
useCallback
與useMemo
極其類似, 可以說是一模一樣,唯一不同的是useMemo
返回的是函數運行的結果,而useCallback
返回的是函數
注意:這個函數是父組件傳遞子組件的一個函數,防止做無關的刷新,其次,這個組件必須配合memo
, 否則不但不會提升性能,還有可能降低性能
import React, { useState, useCallback } from 'react';
import { Button } from 'antd-mobile';
const MockMemo: React.FC<any> = () => {
const [count,setCount] = useState(0)
const [show,setShow] = useState(true)
const add = useCallback(()=>{
setCount(count + 1)
},[count])
return (
<div>
<div style={{display: 'flex', justifyContent: 'flex-start'}}>
<TestButton title="普通點擊" onClick={() => setCount(count + 1) }/>
<TestButton title="useCallback點擊" onClick={add}/>
</div>
<div style={{marginTop: 20}}>count: {count}</div>
<Button onClick={() => {setShow(!show)}}> 切換</Button>
</div>
)
}
const TestButton = React.memo((props:any)=>{
console.log(props.title)
return <Button color='primary' onClick={props.onClick} style={props.title === 'useCallback點擊' ? {
marginLeft: 20
} : undefined}>{props.title}</Button>
})
export default MockMemo;
我們可以看到,當點擊切換按鈕的時候,沒有經過 useCallback
封裝的函數會再次刷新,而進過過 useCallback
包裹的函數不會被再次刷新
useRef
useRef 可以獲取當前元素的所有屬性,並且返回一個可變的 ref 對象,並且這個對象只有 current 屬性,可設置 initialValue
通過 useRef 獲取對應的屬性值
我們先看個案例:
import React, { useState, useRef } from 'react';
const Index:React.FC<any> = () => {
const scrollRef = useRef<any>(null);
const [clientHeight, setClientHeight ] = useState<number>(0)
const [scrollTop, setScrollTop ] = useState<number>(0)
const [scrollHeight, setScrollHeight ] = useState<number>(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 >
<div >
<p>可視區域高度:{clientHeight}</p>
<p>滾動條滾動高度:{scrollTop}</p>
<p>滾動內容高度:{scrollHeight}</p>
</div>
<div style={{height: 200, overflowY: 'auto'}} ref={scrollRef} onScroll={onScroll} >
<div style={{height: 2000}}></div>
</div>
</div>
);
};
export default Index;
從上述可知,我們可以通過useRef
來獲取對應元素的相關屬性,以此來做一些操作
效果:
緩存數據
除了獲取對應的屬性值外,useRef
還有一點比較重要的特性,那就是 緩存數據
上述講到我們封裝一個合格的自定義hooks
的時候需要結合 useMemo、useCallback 等 Api,但我們控制變量的值用 useState 有可能會導致拿到的是舊值,並且如果他們更新會帶來整個組件重新執行,這種情況下,我們使用 useRef 將會是一個非常不錯的選擇
在react-redux
的源碼中,在 hooks 推出後,react-redux
用大量的 useMemo 重做了 Provide 等核心模塊,其中就是運用 useRef 來緩存數據,並且所運用的 useRef() 沒有一個是綁定在 dom 元素上的,都是做數據緩存用的
可以簡單的來看一下:
// 緩存數據
/* react-redux 用userRef 來緩存 merge之後的 props */
const lastChildProps = useRef()
// lastWrapperProps 用 useRef 來存放組件真正的 props信息
const lastWrapperProps = useRef(wrapperProps)
//是否儲存props是否處於正在更新狀態
const renderIsScheduled = useRef(false)
//更新數據
function captureWrapperProps(
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs
) {
lastWrapperProps.current = wrapperProps
lastChildProps.current = actualChildProps
renderIsScheduled.current = false
}
我們看到 react-redux
用重新賦值的方法,改變了緩存的數據源,減少了不必要的更新,如過採取useState
勢必會重新渲染
useLatest
經過上面的講解我們知道useRef
可以拿到最新值,我們可以進行簡單的封裝,這樣做的好處是:可以隨時確保獲取的是最新值,並且也可以解決閉包問題
import { useRef } from 'react';
const useLatest = <T>(value: T) => {
const ref = useRef(value)
ref.current = value
return ref
};
export default useLatest;
結合 useMemo 和 useRef 封裝 useCreation
useCreation :是 useMemo
或 useRef
的替代品。換言之,useCreation
這個鉤子增強了 useMemo
和 useRef
,讓這個鉤子可以替換這兩個鉤子。(來自 ahooks-useCreation[2])
-
useMemo
的值不一定是最新的值,但useCreation
可以保證拿到的值一定是最新的值 -
對於複雜常量的創建,
useRef
容易出現潛在的的性能隱患,但useCreation
可以避免
這裏的性能隱患是指:
// 每次重渲染,都會執行實例化 Subject 的過程,即便這個實例立刻就被扔掉了
const a = useRef(new Subject())
// 通過 factory 函數,可以避免性能隱患
const b = useCreation(() => new Subject(), [])
接下來我們來看看如何封裝一個useCreation
, 首先我們要明白以下三點:
-
第一點:先確定參數,
useCreation
的參數與useMemo
的一致,第一個參數是函數,第二個參數參數是可變的數組 -
第二點:我們的值要保存在
useRef
中,這樣可以將值緩存,從而減少無關的刷新 -
第三點:更新值的判斷,怎麼通過第二個參數來判斷是否更新
useRef
裏的值。
明白了一上三點我們就可以自己實現一個useCreation
import { useRef } from 'react';
import type { DependencyList } from 'react';
const depsAreSame = (oldDeps: DependencyList, deps: DependencyList):boolean => {
if(oldDeps === deps) return true
for(let i = 0; i < oldDeps.length; i++) {
// 判斷兩個值是否是同一個值
if(!Object.is(oldDeps[i], deps[i])) return false
}
return true
}
const useCreation = <T>(fn:() => T, deps: DependencyList)=> {
const { current } = useRef({
deps,
obj: undefined as undefined | T ,
initialized: false
})
if(current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = fn();
current.initialized = true;
}
return current.obj as T
}
export default useCreation;
在useRef
判斷是否更新值通過initialized
和 depsAreSame
來判斷,其中depsAreSame
通過存儲在 useRef
下的deps
(舊值) 和 新傳入的 deps
(新值)來做對比,判斷兩數組的數據是否一致,來確定是否更新
驗證 useCreation
接下來我們寫個小例子,來驗證下 useCreation
是否能滿足我們的要求:
import React, { useState } from 'react';
import { Button } from 'antd-mobile';
import { useCreation } from '@/components';
const Index: React.FC<any> = () => {
const [_, setFlag] = useState<boolean>(false)
const getNowData = () => {
return Math.random()
}
const nowData = useCreation(() => getNowData(), []);
return (
<div style={{padding: 50}}>
<div>正常的函數:{getNowData()}</div>
<div>useCreation包裹後的:{nowData}</div>
<Button color='primary' onClick={() => {setFlag(v => !v)}}> 渲染</Button>
</div>
)
}
export default Index;
我們可以看到,當我們做無關的state
改變的時候,正常的函數也會刷新,但useCreation
沒有刷新,從而增強了渲染的性能~
useEffect
useEffect
相信各位小夥伴已經用的熟的不能再熟了,我們可以使用useEffect
來模擬下class
的componentDidMount
和componentWillUnmount
的功能。
useMount
這個鉤子不必多說,只是簡化了使用useEffect
的第二個參數:
import { useEffect } from 'react';
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.();
}, []);
};
export default useMount;
useUnmount
這個需要注意一個點,就是使用useRef
來確保所傳入的函數爲最新的狀態,所以可以結合上述講的 useLatest 結合使用
import { useEffect, useRef } from 'react';
const useUnmount = (fn: () => void) => {
const ref = useRef(fn);
ref.current = fn;
useEffect(
() => () => {
fn?.()
},
[],
);
};
export default useUnmount;
結合useMount
和useUnmount
做個小例子
import { Button, Toast } from 'antd-mobile';
import React,{ useState } from 'react';
import { useMount, useUnmount } from '@/components';
const Child = () => {
useMount(() => {
Toast.show('首次渲染')
});
useUnmount(() => {
Toast.show('組件已卸載')
})
return <div>你好,我是小杜杜</div>
}
const Index:React.FC<any> = (props)=> {
const [flag, setFlag] = useState<boolean>(false)
return (
<div style={{padding: 50}}>
<Button color='primary' onClick={() => {setFlag(v => !v)}}>切換 {flag ? 'unmount' : 'mount'}</Button>
{flag && <Child />}
</div>
);
}
export default Index;
效果如下:
useUpdate
useUpdate: 強制更新
有的時候我們需要組件強制更新,這個時候就可以使用這個鉤子:
import { useCallback, useState } from 'react';
const useUpdate = () => {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
};
export default useUpdate;
//示例:
import { Button } from 'antd-mobile';
import React from 'react';
import { useUpdate } from '@/components';
const Index:React.FC<any> = (props)=> {
const update = useUpdate();
return (
<div style={{padding: 50}}>
<div>時間:{Date.now()}</div>
<Button color='primary' onClick={update}>更新時間</Button>
</div>
);
}
export default Index;
效果如下:
案例
案例 1: useReactive
useReactiv: 一種具備響應式的useState
緣由:我們知道用useState
可以定義變量其格式爲:
const [count, setCount] = useState<number>(0)
通過setCount
來設置,count
來獲取,使用這種方式才能夠渲染視圖
來看看正常的操作,像這樣 let count = 0; count =7
此時count
的值就是 7,也就是說數據是響應式的
那麼我們可不可以將 useState
也寫成響應式的呢?我可以自由設置 count 的值, 並且可以隨時獲取到 count 的最新值,而不是通過setCount
來設置。
我們來想想怎麼去實現一個具備 響應式 特點的 useState
也就是 useRective
, 提出以下疑問,感興趣的,可以先自行思考一下:
-
這個鉤子的出入參該怎麼設定?
-
如何將數據製作成響應式(畢竟普通的操作無法刷新視圖)?
-
如何使用
TS
去寫,完善其類型? -
如何更好的去優化?
分析
以上四個小問題,最關鍵的就是第二個
,我們如何將數據弄成響應式,想要弄成響應式,就必須監聽到值的變化,在做出更改,也就是說,我們對這個數進行操作的時候,要進行相應的攔截,這時就需要ES6
的一個知識點:Proxy
在這裏會用到 Proxy 和 Reflect 的點,感興趣的可以看看我的這篇文章:🔥花一個小時,迅速瞭解 ES6~ES12 的全部特性 [3]
Proxy:接受的參數是對象,所以第一個問題也解決了,入參就爲對象。那麼如何去刷新視圖呢?這裏就使用上述的 useUpdate 來強制刷新,使數據更改。
至於優化這一塊,使用上文說的useCreation
就好,再配合useRef
來放initialState
即可
代碼
import { useRef } from 'react';
import { useUpdate, useCreation } from '../index';
const observer = <T extends Record<string, any>>(initialVal: T, cb: () => void): T => {
const proxy = new Proxy<T>(initialVal, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
return typeof res === 'object' ? observer(res, cb) : Reflect.get(target, key);
},
set(target, key, val) {
const ret = Reflect.set(target, key, val);
cb();
return ret;
},
});
return proxy;
}
const useReactive = <T extends Record<string, any>>(initialState: T):T => {
const ref = useRef<T>(initialState);
const update = useUpdate();
const state = useCreation(() => {
return observer(ref.current, () => {
update();
});
}, []);
return state
};
export default useReactive;
這裏先說下TS
,因爲我們不知道會傳遞什麼類型的initialState
所以在這需要使用泛型,我們接受的參數是對象,可就是 key-value 的形式,其中 key 爲 string,value 可以是 任意類型,所以我們使用 Record<string, any>
有不熟悉的小夥伴可以看看我的這篇文章:一篇讓你完全夠用 TS 的指南 [4](又推銷一遍,有點打廣告,別在意~)
再來說下攔截這塊
, 我們只需要攔截設置(set) 和 獲取(get) 即可,其中:
-
設置這塊,需要改變是圖,也就是說需要,使用 useUpdate 來強制刷新
-
獲取這塊,需要判斷其是否爲對象,是的話繼續遞歸,不是的話返回就行
驗證
接下來我們來驗證一下我們寫的 useReactive
, 我們將以 字符串、數字、布爾、數組、函數、計算屬性幾個方面去驗證一下:
import { Button } from 'antd-mobile';
import React from 'react';
import { useReactive } from '@/components'
const Index:React.FC<any> = (props)=> {
const state = useReactive<any>({
count: 0,
name: '小杜杜',
flag: true,
arr: [],
bugs: ['小杜杜', 'react', 'hook'],
addBug(bug:string) {
this.bugs.push(bug);
},
get bugsCount() {
return this.bugs.length;
},
})
return (
<div style={{padding: 20}}>
<div style={{fontWeight: 'bold'}}>基本使用:</div>
<div style={{marginTop: 8}}> 對數字進行操作:{state.count}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color='primary' onClick={() => state.count++ } >加1</Button>
<Button color='primary' style={{marginLeft: 8}} onClick={() => state.count-- } >減1</Button>
<Button color='primary' style={{marginLeft: 8}} onClick={() => state.count = 7 } >設置爲7</Button>
</div>
<div style={{marginTop: 8}}> 對字符串進行操作:{state.name}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color='primary' onClick={() => state.name = '小杜杜' } >設置爲小杜杜</Button>
<Button color='primary' style={{marginLeft: 8}} onClick={() => state.name = 'Domesy'} >設置爲Domesy</Button>
</div>
<div style={{marginTop: 8}}> 對布爾值進行操作:{JSON.stringify(state.flag)}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color='primary' onClick={() => state.flag = !state.flag } >切換狀態</Button>
</div>
<div style={{marginTop: 8}}> 對數組進行操作:{JSON.stringify(state.arr)}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color="primary" onClick={() => state.arr.push(Math.floor(Math.random() * 100))} >push</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.pop()} >pop</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.shift()} >shift</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.unshift(Math.floor(Math.random() * 100))} >unshift</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.reverse()} >reverse</Button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.arr.sort()} >sort</Button>
</div>
<div style={{fontWeight: 'bold', marginTop: 8}}>計算屬性:</div>
<div style={{marginTop: 8}}>數量:{ state.bugsCount } 個</div>
<div style={{margin: '8px 0'}}>
<form
onSubmit={(e) => {
state.bug ? state.addBug(state.bug) : state.addBug('domesy')
state.bug = '';
e.preventDefault();
}}
>
<input type="text" value={state.bug} onChange={(e) => (state.bug = e.target.value)} />
<button type="submit" style={{marginLeft: 8}} >增加</button>
<Button color="primary" style={{marginLeft: 8}} onClick={() => state.bugs.pop()}>刪除</Button>
</form>
</div>
<ul>
{
state.bugs.map((bug:any, index:number) => (
<li key={index}>{bug}</li>
))
}
</ul>
</div>
);
}
export default Index;
效果如下:
案例 2: useEventListener
緣由:我們監聽各種事件的時候需要做監聽,如:監聽點擊事件、鍵盤事件、滾動事件等,我們將其統一封裝起來,方便後續調用
說白了就是在addEventListener
的基礎上進行封裝,我們先來想想在此基礎上需要什麼?
首先,useEventListener
的入參可分爲三個
-
第一個
event
是事件(如:click、keydown) -
第二個回調函數(所以不需要出參)
-
第三個就是目標(是某個節點還是全局)
在這裏需要注意一點就是在銷燬的時候需要移除對應的監聽事件
代碼
import { useEffect } from 'react';
const useEventListener = (event: string, handler: (...e:any) => void, target: any = window) => {
useEffect(() => {
const targetElement = 'current' in target ? target.current : window;
const useEventListener = (event: Event) => {
return handler(event)
}
targetElement.addEventListener(event, useEventListener)
return () => {
targetElement.removeEventListener(event, useEventListener)
}
}, [event])
};
export default useEventListener;
注:這裏把target
默認設置成了window
,至於爲什麼要這麼寫:'current' in target
是因爲我們用useRef
拿到的值都是 ref.current
優化
接下來我們一起來看看如何優化這個組件,這裏的優化與 useCreation
類似,但又有不同,原因是這裏的需要判斷的要比useCreation
複雜一點。
再次強調一下,傳遞過來的值,優先考慮使用
useRef
,再考慮用useState
,可以直接使用useLatest
,防止拿到的值不是最新值
這裏簡單說一下我的思路(又不對的地方或者有更好的建議歡迎評論區指出):
-
首先需要
hasInitRef
來存儲是否是第一次進入,通過它來判斷初始化存儲 -
然後考慮有幾個參數需要存儲,從上述代碼上來看,可變的變量有兩個,一個是
event
,另一個是target
,其次,我們還需要存儲對應的卸載後的函數
,所以存儲的變量應該有3個
-
接下來考慮一下什麼情況下觸發更新,也就是可變的兩個參數:
event
和target
-
最後在卸載的時候可以考慮使用
useUnmount
,並執行存儲對應的卸載後的函數
和把hasInitRef
還原
詳細代碼
import { useEffect } from 'react';
import type { DependencyList } from 'react';
import { useRef } from 'react';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
const depsAreSame = (oldDeps: DependencyList, deps: DependencyList):boolean => {
for(let i = 0; i < oldDeps.length; i++) {
if(!Object.is(oldDeps[i], deps[i])) return false
}
return true
}
const useEffectTarget = (effect: () => void, deps:DependencyList, target: any) => {
const hasInitRef = useRef(false); // 一開始設置初始化
const elementRef = useRef<(Element | null)[]>([]);// 存儲具體的值
const depsRef = useRef<DependencyList>([]); // 存儲傳遞的deps
const unmountRef = useRef<any>(); // 存儲對應的effect
// 初始化 組件的初始化和更新都會執行
useEffect(() => {
const targetElement = 'current' in target ? target.current : window;
// 第一遍賦值
if(!hasInitRef.current){
hasInitRef.current = true;
elementRef.current = targetElement;
depsRef.current = deps;
unmountRef.current = effect();
return
}
// 校驗變值: 目標的值不同, 依賴值改變
if(elementRef.current !== targetElement || !depsAreSame(deps, depsRef.current)){
//先執行對應的函數
unmountRef.current?.();
//重新進行賦值
elementRef.current = targetElement;
depsRef.current = deps;
unmountRef.current = effect();
}
})
useUnmount(() => {
unmountRef.current?.();
hasInitRef.current = false;
})
}
const useEventListener = (event: string, handler: (...e:any) => void, target: any = window) => {
const handlerRef = useLatest(handler);
useEffectTarget(() => {
const targetElement = 'current' in target ? target.current : window;
// 防止沒有 addEventListener 這個屬性
if(!targetElement?.addEventListener) return;
const useEventListener = (event: Event) => {
return handlerRef.current(event)
}
targetElement.addEventListener(event, useEventListener)
return () => {
targetElement.removeEventListener(event, useEventListener)
}
}, [event], target)
};
export default useEventListener;
-
在這裏只用
useEffect
是因爲,在更新和初始化的情況下都需要使用 -
必須要防止沒有
addEventListener
這個屬性的情況,監聽的目標有可能沒有加載出來
驗證
驗證一下useEventListener
是否能夠正常的使用,順變驗證一下初始化、卸載的,代碼:
import React, { useState, useRef } from 'react';
import { useEventListener } from '@/components'
import { Button } from 'antd-mobile';
const Index:React.FC<any> = (props)=> {
const [count, setCount] = useState<number>(0)
const [flag, setFlag] = useState<boolean>(true)
const [key, setKey] = useState<string>('')
const ref = useRef(null);
useEventListener('click', () => setCount(v => v +1), ref)
useEventListener('keydown', (ev) => setKey(ev.key));
return (
<div style={{padding: 20}}>
<Button color='primary' onClick={() => {setFlag(v => !v)}}>切換 {flag ? 'unmount' : 'mount'}</Button>
{
flag && <div>
<div>數字:{count}</div>
<button ref={ref} >加1</button>
<div>監聽鍵盤事件:{key}</div>
</div>
}
</div>
);
}
export default Index;
效果:
我們可以利用useEventListener
這個鉤子去封裝其他鉤子,如 鼠標懸停,長按事件,鼠標位置等,在這裏在舉一個鼠標懸停的小例子
小例子 useHover
useHover:監聽 DOM 元素是否有鼠標懸停
這個就很簡單了,只需要通過 useEventListener
來監聽mouseenter
和mouseleave
即可,在返回布爾值就行了:
import { useState } from 'react';
import useEventListener from '../useEventListener';
interface Options {
onEnter?: () => void;
onLeave?: () => void;
}
const useHover = (target:any, options?:Options): boolean => {
const [flag, setFlag] = useState<boolean>(false)
const { onEnter, onLeave } = options || {};
useEventListener('mouseenter', () => {
onEnter?.()
setFlag(true)
}, target)
useEventListener('mouseleave', () => {
onLeave?.()
setFlag(false)
}, target)
return flag
};
export default useHover;
效果:
案例 3: 有關時間的 Hooks
在這裏主要介紹有關時間的三個 hooks, 分別是:useTimeout
、useInterval
和useCountDown
useTimeout
useTimeout:一段時間內,執行一次
傳遞參數只要函數和延遲時間即可,需要注意的是卸載的時候將定時器清除下就 OK 了
詳細代碼:
import { useEffect } from 'react';
import useLatest from '../useLatest';
const useTimeout = (fn:() => void, delay?: number): void => {
const fnRef = useLatest(fn)
useEffect(() => {
if(!delay || delay < 0) return;
const timer = setTimeout(() => {
fnRef.current();
}, delay)
return () => {
clearTimeout(timer)
}
}, [delay])
};
export default useTimeout;
效果展示:
useInterval
useInterval: 每過一段時間內一直執行
大體上與useTimeout
一樣,多了一個是否要首次渲染的參數immediate
詳細代碼:
import { useEffect } from 'react';
import useLatest from '../useLatest';
const useInterval = (fn:() => void, delay?: number, immediate?:boolean): void => {
const fnRef = useLatest(fn)
useEffect(() => {
if(!delay || delay < 0) return;
if(immediate) fnRef.current();
const timer = setInterval(() => {
fnRef.current();
}, delay)
return () => {
clearInterval(timer)
}
}, [delay])
};
export default useInterval;
效果展示:
useCountDown
useCountDown:簡單控制倒計時的鉤子
跟之前一樣我們先來想想這個鉤子需要什麼:
-
我們要做倒計時的鉤子首先需要一個目標時間(targetDate),控制時間變化的秒數(interval 默認爲 1s),然後就是倒計時完成後所觸發的函數(onEnd)
-
返參就更加一目瞭然了,返回的是兩個時間差的數值(time),再詳細點可以換算成對應的天、時、分等(formattedRes)
詳細代碼
import { useState, useEffect, useMemo } from 'react';
import useLatest from '../useLatest';
import dayjs from 'dayjs';
type DTime = Date | number | string | undefined;
interface Options {
targetDate?: DTime;
interval?: number;
onEnd?: () => void;
}
interface FormattedRes {
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}
const calcTime = (time: DTime) => {
if(!time) return 0
const res = dayjs(time).valueOf() - new Date().getTime(); //計算差值
if(res < 0) return 0
return res
}
const parseMs = (milliseconds: number): FormattedRes => {
return {
days: Math.floor(milliseconds / 86400000),
hours: Math.floor(milliseconds / 3600000) % 24,
minutes: Math.floor(milliseconds / 60000) % 60,
seconds: Math.floor(milliseconds / 1000) % 60,
milliseconds: Math.floor(milliseconds) % 1000,
};
};
const useCountDown = (options?: Options) => {
const { targetDate, interval = 1000, onEnd } = options || {};
const [time, setTime] = useState(() => calcTime(targetDate));
const onEndRef = useLatest(onEnd);
useEffect(() => {
if(!targetDate) return setTime(0)
setTime(calcTime(targetDate))
const timer = setInterval(() => {
const target = calcTime(targetDate);
setTime(target);
if (target === 0) {
clearInterval(timer);
onEndRef.current?.();
}
}, interval);
return () => clearInterval(timer);
},[targetDate, interval])
const formattedRes = useMemo(() => {
return parseMs(time);
}, [time]);
return [time, formattedRes] as const
};
export default useCountDown;
驗證
import React, { useState } from 'react';
import { useCountDown } from '@/components'
import { Button, Toast } from 'antd-mobile';
const Index:React.FC<any> = (props)=> {
const [_, formattedRes] = useCountDown({
targetDate: '2022-12-31 24:00:00',
});
const { days, hours, minutes, seconds, milliseconds } = formattedRes;
const [count, setCount] = useState<number>();
const [countdown] = useCountDown({
targetDate: count,
onEnd: () => {
Toast.show('結束')
},
});
return (
<div style={{padding: 20}}>
<div> 距離 2022-12-31 24:00:00 還有 {days} 天 {hours} 時 {minutes} 分 {seconds} 秒 {milliseconds} 毫秒</div>
<div>
<p style={{marginTop: 12}}>動態變化:</p>
<Button color='primary' disabled={countdown !== 0} onClick={() => setCount(Date.now() + 3000)}>
{countdown === 0 ? '開始' : `還有 ${Math.round(countdown / 1000)}s`}
</Button>
<Button style={{marginLeft: 8}} onClick={() => setCount(undefined)}>停止</Button>
</div>
</div>
);
}
export default Index;
效果展示:
End
參考
- ahooks[5]
總結
簡單的做下總結:
-
一個優秀的 hooks 一定會具備
useMemo
、useCallback
等 api 優化 -
製作自定義 hooks 遇到傳遞過來的值,優先考慮使用
useRef
,再考慮用useState
,可以直接使用useLatest
,防止拿到的值不是最新值 -
在封裝的時候,應該將存放的值放入
useRef
中,通過一個狀態去設置他的初始化,在判斷什麼情況下來更新所對應的值,明確入參與出參的具體意義,如useCreation
和useEventListener
盤點
本文一共講解了 12 個自定義 hooks,分別是:usePow
、useLatest
、useCreation
、useMount
、useUnmount
、useUpdate
、useReactive
、useEventListener
、useHover
、useTimeout
、useInterval
、useCountDown
這裏的素材來源爲 ahooks,但與 ahooks 的不是完全一樣,有興趣的小夥伴可以結合ahooks
源碼對比來看,自己動手敲敲,加深理解
相信在這篇文章的幫助下,各位小夥伴應該跟我一樣對Hooks
有了更深的理解,當然,實踐是檢驗真理的唯一標準,多多敲代碼纔是王道~
另外,覺得這篇文章能夠幫助到你的話,請點贊 + 收藏一下吧,順便關注下專欄,之後會輸出有關React
的好文,一起上車學習吧~
react
其他好文:「React 深入」這就是 HOC,這次我終於悟了!!![6]
關於本文
來自:小杜杜
https://juejin.cn/post/7101486767336849421
參考資料
[1]
https://juejin.cn/post/7088304364078497800: https://juejin.cn/post/7088304364078497800
[2]
https://ahooks.js.org/zh-CN/hooks/use-creation: https://link.juejin.cn?target=https%3A%2F%2Fahooks.js.org%2Fzh-CN%2Fhooks%2Fuse-creation
[3]
https://juejin.cn/post/7068935394191998990#heading-36: https://juejin.cn/post/7068935394191998990#heading-36
[4]
https://juejin.cn/post/7088304364078497800#heading-82: https://juejin.cn/post/7088304364078497800#heading-82
[5]
https://ahooks.js.org/zh-CN/hooks/use-request/index: https://link.juejin.cn?target=https%3A%2F%2Fahooks.js.org%2Fzh-CN%2Fhooks%2Fuse-request%2Findex
[6]
https://juejin.cn/post/7103345085089054727: https://juejin.cn/post/7103345085089054727
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Bdv9Kx9zFqm76OVXjPKq9Q