搞懂這12個Hooks,保證讓你玩轉(zhuǎn)React
React Hooks的發(fā)布已經(jīng)有三年多了,它給函數(shù)式組件帶來了生命周期,現(xiàn)如今,Hooks逐漸取代class組件,相信各位 React 開發(fā)的小伙伴已經(jīng)深有體會,然而你真的完全掌握hooks了嗎?知道如何去做一個好的自定義hooks嗎?
我們知道React Hooks有useState設置變量,useEffect副作用,useRef來獲取元素的所有屬性,還有useMemo、useCallback來做性能優(yōu)化,當然還有一個自定義Hooks,來創(chuàng)造出你所想要的Hooks
接下來我們來看看以下幾個問題,問問自己,是否全都知道:
Hooks的由來是什么? useRef的高級用法是什么?useMemo和useCallback是怎么做優(yōu)化的?一個好的自定義Hooks該如何設計? 如何做一個不需要 useState就可以直接修改屬性并刷新視圖的自定義Hooks?如何做一個可以監(jiān)聽任何事件的自定義Hooks?
如果你對以上問題有疑問,有好奇,那么這篇文章應該能夠幫助到你~
本文將會以介紹自定義Hooks來解答上述問題,并結合 TS,ahooks中的鉤子,以案列的形式去演示,本文過長,建議:點贊 + 收藏 哦~
注:這里講解的自定義鉤子可能會和 ahooks上的略有不同,不會考慮過多的情況,如果用于項目,建議直接使用ahooks上的鉤子~
如果有小伙伴不懂TS,可以看看我的這篇文章:一篇讓你完全夠用TS的指南[1]
先附上一張今天的知識圖,還請各位小伙伴多多支持:

自定義Hooks是什么?
react-hooks是React16.8以后新增的鉤子API,目的是增加代碼的可復用性、邏輯性,最主要的是解決了函數(shù)式組件無狀態(tài)的問題,這樣既保留了函數(shù)式的簡單,又解決了沒有數(shù)據(jù)管理狀態(tài)的缺陷
那么什么是自定義hooks呢?
自定義hooks是在react-hooks基礎上的一個擴展,可以根據(jù)業(yè)務、需求去制定相應的hooks,將常用的邏輯進行封裝,從而具備復用性
如何設計一個自定義Hooks
hooks本質(zhì)上是一個函數(shù),而這個函數(shù)主要就是邏輯復用,我們首先要知道一件事,hooks的驅(qū)動條件是什么?
其實就是props的修改,useState、useReducer的使用是無狀態(tài)組件更新的條件,從而驅(qū)動自定義hooks
通用模式
自定義hooks的名稱是以use開頭,我們設計為:
const [ xxx, ...] = useXXX(參數(shù)一,參數(shù)二...)
簡單的小例子: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>數(shù)字:{JSON.stringify(data)}</div>
<Button color='primary' onClick={() => {setFlag(v => !v)}}>切換</Button>
<div>切換狀態(tài):{JSON.stringify(flag)}</div>
</div>
);
}
export default Index;
我們簡單的寫了個 usePow,我們通過 usePow 給所傳入的數(shù)字平方, 用切換狀態(tài)的按鈕表示函數(shù)內(nèi)部的狀態(tài),我們來看看此時的效果:

我們發(fā)現(xiàn)了一個問題,為什么點擊切換按鈕也會觸發(fā)console.log(1)呢?
這樣明顯增加了性能開銷,我們的理想狀態(tài)肯定不希望做無關的渲染,所以我們做自定義 hooks的時候一定要注意,需要減少性能開銷,我們?yōu)榻M件加入 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;

發(fā)現(xiàn)此時就已經(jīng)解決了這個問題,所以要非常注意一點,一個好用的自定義hooks,一定要配合useMemo、useCallback等 Api 一起使用。
玩轉(zhuǎn)React Hooks
在上述中我們講了用 useMemo來處理無關的渲染,接下來我們一起來看看React Hooks的這些鉤子的妙用(這里建議先熟知、并使用對應的React Hooks,才能造出好的鉤子)
useMemo
當一個父組件中調(diào)用了一個子組件的時候,父組件的 state 發(fā)生變化,會導致父組件更新,而子組件雖然沒有發(fā)生改變,但也會進行更新。
簡單的理解下,當一個頁面內(nèi)容非常復雜,模塊非常多的時候,函數(shù)式組件會從頭更新到尾,只要一處改變,所有的模塊都會進行刷新,這種情況顯然是沒有必要的。
我們理想的狀態(tài)是各個模塊只進行自己的更新,不要相互去影響,那么此時用useMemo是最佳的解決方案。
這里要尤其注意一點,只要父組件的狀態(tài)更新,無論有沒有對自組件進行操作,子組件都會進行更新,useMemo就是為了防止這點而出現(xiàn)的
在講 useMemo 之前,我們先說說memo,memo的作用是結合了pureComponent純組件和 componentShouldUpdate功能,會對傳入的props進行一次對比,然后根據(jù)第二個函數(shù)返回值來進一步判斷哪些props需要更新。(具體使用會在下文講到~)
useMemo與memo的理念上差不多,都是判斷是否滿足當前的限定條件來決定是否執(zhí)行callback函數(shù),而useMemo的第二個參數(shù)是一個數(shù)組,通過這個數(shù)組來判定是否更新回掉函數(shù)
這種方式可以運用在元素、組件、上下文中,尤其是利用在數(shù)組上,先看一個例子:
useMemo(() => (
<div>
{
list.map((item, index) => (
<p key={index}>
{item.name}
</>
)}
}
</div>
),[list])
從上面我們看出 useMemo只有在list發(fā)生變化的時候才會進行渲染,從而減少了不必要的開銷
總結一下useMemo的好處:
可以減少不必要的循環(huán)和不必要的渲染 可以減少子組件的渲染次數(shù) 通過特地的依賴進行更新,可以避免很多不必要的開銷,但要注意,有時候在配合 useState拿不到最新的值,這種情況可以考慮使用useRef解決
useCallback
useCallback與useMemo極其類似,可以說是一模一樣,唯一不同的是useMemo返回的是函數(shù)運行的結果,而useCallback返回的是函數(shù)
注意:這個函數(shù)是父組件傳遞子組件的一個函數(shù),防止做無關的刷新,其次,這個組件必須配合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;

我們可以看到,當點擊切換按鈕的時候,沒有經(jīng)過 useCallback封裝的函數(shù)會再次刷新,而進過過 useCallback包裹的函數(shù)不會被再次刷新
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; //可視區(qū)域高度
let scrollTop = scrollRef?.current.scrollTop; //滾動條滾動高度
let scrollHeight = scrollRef?.current.scrollHeight; //滾動內(nèi)容高度
setClientHeight(clientHeight)
setScrollTop(scrollTop)
setScrollHeight(scrollHeight)
}
}
return (
<div >
<div >
<p>可視區(qū)域高度:{clientHeight}</p>
<p>滾動條滾動高度:{scrollTop}</p>
<p>滾動內(nèi)容高度:{scrollHeight}</p>
</div>
<div style={{height: 200, overflowY: 'auto'}} ref={scrollRef} onScroll={onScroll} >
<div style={{height: 2000}}></div>
</div>
</div>
);
};
export default Index;
從上述可知,我們可以通過useRef來獲取對應元素的相關屬性,以此來做一些操作
效果:
緩存數(shù)據(jù)
除了獲取對應的屬性值外,useRef還有一點比較重要的特性,那就是 緩存數(shù)據(jù)
上述講到我們封裝一個合格的自定義hooks的時候需要結合useMemo、useCallback等Api,但我們控制變量的值用useState 有可能會導致拿到的是舊值,并且如果他們更新會帶來整個組件重新執(zhí)行,這種情況下,我們使用useRef將會是一個非常不錯的選擇
在react-redux的源碼中,在hooks推出后,react-redux用大量的useMemo重做了Provide等核心模塊,其中就是運用useRef來緩存數(shù)據(jù),并且所運用的 useRef() 沒有一個是綁定在dom元素上的,都是做數(shù)據(jù)緩存用的
可以簡單的來看一下:
// 緩存數(shù)據(jù)
/* react-redux 用userRef 來緩存 merge之后的 props */
const lastChildProps = useRef()
// lastWrapperProps 用 useRef 來存放組件真正的 props信息
const lastWrapperProps = useRef(wrapperProps)
//是否儲存props是否處于正在更新狀態(tài)
const renderIsScheduled = useRef(false)
//更新數(shù)據(jù)
function captureWrapperProps(
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs
) {
lastWrapperProps.current = wrapperProps
lastChildProps.current = actualChildProps
renderIsScheduled.current = false
}
我們看到 react-redux 用重新賦值的方法,改變了緩存的數(shù)據(jù)源,減少了不必要的更新,如過采取useState勢必會重新渲染
useLatest
經(jīng)過上面的講解我們知道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可以保證拿到的值一定是最新的值對于復雜常量的創(chuàng)建, useRef容易出現(xiàn)潛在的的性能隱患,但useCreation可以避免
這里的性能隱患是指:
// 每次重渲染,都會執(zhí)行實例化 Subject 的過程,即便這個實例立刻就被扔掉了
const a = useRef(new Subject())
// 通過 factory 函數(shù),可以避免性能隱患
const b = useCreation(() => new Subject(), [])
接下來我們來看看如何封裝一個useCreation,首先我們要明白以下三點:
第一點:先確定參數(shù), useCreation的參數(shù)與useMemo的一致,第一個參數(shù)是函數(shù),第二個參數(shù)參數(shù)是可變的數(shù)組第二點:我們的值要保存在 useRef中,這樣可以將值緩存,從而減少無關的刷新第三點:更新值的判斷,怎么通過第二個參數(shù)來判斷是否更新 useRef里的值。
明白了一上三點我們就可以自己實現(xiàn)一個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(新值)來做對比,判斷兩數(shù)組的數(shù)據(jù)是否一致,來確定是否更新
驗證 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>正常的函數(shù):{getNowData()}</div>
<div>useCreation包裹后的:{nowData}</div>
<Button color='primary' onClick={() => {setFlag(v => !v)}}> 渲染</Button>
</div>
)
}
export default Index;
我們可以看到,當我們做無關的state改變的時候,正常的函數(shù)也會刷新,但useCreation沒有刷新,從而增強了渲染的性能~
useEffect
useEffect相信各位小伙伴已經(jīng)用的熟的不能再熟了,我們可以使用useEffect來模擬下class的componentDidMount和componentWillUnmount的功能。
useMount
這個鉤子不必多說,只是簡化了使用useEffect的第二個參數(shù):
import { useEffect } from 'react';
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.();
}, []);
};
export default useMount;
useUnmount
這個需要注意一個點,就是使用useRef來確保所傳入的函數(shù)為最新的狀態(tài),所以可以結合上述講的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,也就是說數(shù)據(jù)是響應式的
那么我們可不可以將 useState也寫成響應式的呢?我可以自由設置count的值,并且可以隨時獲取到count的最新值,而不是通過setCount來設置。
我們來想想怎么去實現(xiàn)一個具備 響應式 特點的 useState 也就是 useRective,提出以下疑問,感興趣的,可以先自行思考一下:
這個鉤子的出入?yún)⒃撛趺丛O定? 如何將數(shù)據(jù)制作成響應式(畢竟普通的操作無法刷新視圖)? 如何使用 TS去寫,完善其類型?如何更好的去優(yōu)化?
分析
以上四個小問題,最關鍵的就是第二個,我們?nèi)绾螌?shù)據(jù)弄成響應式,想要弄成響應式,就必須監(jiān)聽到值的變化,在做出更改,也就是說,我們對這個數(shù)進行操作的時候,要進行相應的攔截,這時就需要ES6的一個知識點:Proxy
在這里會用到 Proxy和Reflect的點,感興趣的可以看看我的這篇文章:??花一個小時,迅速了解ES6\~ES12的全部特性[3]
Proxy:接受的參數(shù)是對象,所以第一個問題也解決了,入?yún)⒕蜑閷ο?。那么如何去刷新視圖呢?這里就使用上述的useUpdate來強制刷新,使數(shù)據(jù)更改。
至于優(yōu)化這一塊,使用上文說的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所以在這需要使用泛型,我們接受的參數(shù)是對象,可就是 key-value 的形式,其中 key 為 string,value 可以是 任意類型,所以我們使用 Record<string, any>
有不熟悉的小伙伴可以看看我的這篇文章:一篇讓你完全夠用TS的指南[4](又推銷一遍,有點打廣告,別在意~)
再來說下攔截這塊,我們只需要攔截設置(set) 和 獲取(get) 即可,其中:
設置這塊,需要改變是圖,也就是說需要,使用useUpdate來強制刷新 獲取這塊,需要判斷其是否為對象,是的話繼續(xù)遞歸,不是的話返回就行
驗證
接下來我們來驗證一下我們寫的 useReactive,我們將以 字符串、數(shù)字、布爾、數(shù)組、函數(shù)、計算屬性幾個方面去驗證一下:
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}}> 對數(shù)字進行操作:{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 } >切換狀態(tài)</Button>
</div>
<div style={{marginTop: 8}}> 對數(shù)組進行操作:{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}}>數(shù)量:{ 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
緣由:我們監(jiān)聽各種事件的時候需要做監(jiān)聽,如:監(jiān)聽點擊事件、鍵盤事件、滾動事件等,我們將其統(tǒng)一封裝起來,方便后續(xù)調(diào)用
說白了就是在addEventListener的基礎上進行封裝,我們先來想想在此基礎上需要什么?
首先,useEventListener的入?yún)⒖煞譃槿齻€
第一個 event是事件(如:click、keydown)第二個回調(diào)函數(shù)(所以不需要出參) 第三個就是目標(是某個節(jié)點還是全局)
在這里需要注意一點就是在銷毀的時候需要移除對應的監(jiān)聽事件
代碼
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
優(yōu)化
接下來我們一起來看看如何優(yōu)化這個組件,這里的優(yōu)化與 useCreation 類似,但又有不同,原因是這里的需要判斷的要比useCreation復雜一點。
再次強調(diào)一下,傳遞過來的值,優(yōu)先考慮使用
useRef,再考慮用useState,可以直接使用useLatest,防止拿到的值不是最新值
這里簡單說一下我的思路(又不對的地方或者有更好的建議歡迎評論區(qū)指出):
首先需要 hasInitRef來存儲是否是第一次進入,通過它來判斷初始化存儲然后考慮有幾個參數(shù)需要存儲,從上述代碼上來看,可變的變量有兩個,一個是 event,另一個是target,其次,我們還需要存儲對應的卸載后的函數(shù),所以存儲的變量應該有3個接下來考慮一下什么情況下觸發(fā)更新,也就是可變的兩個參數(shù): event和target最后在卸載的時候可以考慮使用 useUnmount,并執(zhí)行存儲對應的卸載后的函數(shù)和把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
// 初始化 組件的初始化和更新都會執(zhí)行
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)){
//先執(zhí)行對應的函數(shù)
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這個屬性的情況,監(jiān)聽的目標有可能沒有加載出來
驗證
驗證一下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>數(shù)字:{count}</div>
<button ref={ref} >加1</button>
<div>監(jiān)聽鍵盤事件:{key}</div>
</div>
}
</div>
);
}
export default Index;
效果:

我們可以利用useEventListener這個鉤子去封裝其他鉤子,如 鼠標懸停,長按事件,鼠標位置等,在這里在舉一個鼠標懸停的小例子
小例子 useHover
useHover:監(jiān)聽 DOM 元素是否有鼠標懸停
這個就很簡單了,只需要通過 useEventListener來監(jiān)聽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:一段時間內(nèi),執(zhí)行一次
傳遞參數(shù)只要函數(shù)和延遲時間即可,需要注意的是卸載的時候?qū)⒍〞r器清除下就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: 每過一段時間內(nèi)一直執(zhí)行
大體上與useTimeout一樣,多了一個是否要首次渲染的參數(shù)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),控制時間變化的秒數(shù)(interval默認為1s),然后就是倒計時完成后所觸發(fā)的函數(shù)(onEnd) 返參就更加一目了然了,返回的是兩個時間差的數(shù)值(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}}>動態(tài)變化:</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]
總結
簡單的做下總結:
一個優(yōu)秀的hooks一定會具備 useMemo、useCallback等api優(yōu)化制作自定義hooks遇到傳遞過來的值,優(yōu)先考慮使用 useRef,再考慮用useState,可以直接使用useLatest,防止拿到的值不是最新值在封裝的時候,應該將存放的值放入 useRef中,通過一個狀態(tài)去設置他的初始化,在判斷什么情況下來更新所對應的值,明確入?yún)⑴c出參的具體意義,如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
往期干貨:
26個經(jīng)典微信小程序+35套微信小程序源碼+微信小程序合集源碼下載(免費)
干貨~~~2021最新前端學習視頻~~速度領取
前端書籍-前端290本高清pdf電子書打包下載
點贊和在看就是最大的支持??
