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

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

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

我們可以看到,當(dāng)點(diǎn)擊切換按鈕的時(shí)候,沒(méi)有經(jīng)過(guò) useCallback封裝的函數(shù)會(huì)再次刷新,而進(jìn)過(guò)過(guò) useCallback包裹的函數(shù)不會(huì)被再次刷新
useRef
useRef 可以獲取當(dāng)前元素的所有屬性,并且返回一個(gè)可變的ref對(duì)象,并且這個(gè)對(duì)象只有current屬性,可設(shè)置initialValue
通過(guò)useRef獲取對(duì)應(yīng)的屬性值
我們先看個(gè)案例:
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; //滾動(dòng)條滾動(dòng)高度
let scrollHeight = scrollRef?.current.scrollHeight; //滾動(dòng)內(nèi)容高度
setClientHeight(clientHeight)
setScrollTop(scrollTop)
setScrollHeight(scrollHeight)
}
}
return (
<div >
<div >
<p>可視區(qū)域高度:{clientHeight}</p>
<p>滾動(dòng)條滾動(dòng)高度:{scrollTop}</p>
<p>滾動(dòng)內(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;
從上述可知,我們可以通過(guò)useRef來(lái)獲取對(duì)應(yīng)元素的相關(guān)屬性,以此來(lái)做一些操作
效果:
緩存數(shù)據(jù)
除了獲取對(duì)應(yīng)的屬性值外,useRef還有一點(diǎn)比較重要的特性,那就是 緩存數(shù)據(jù)
上述講到我們封裝一個(gè)合格的自定義hooks的時(shí)候需要結(jié)合useMemo、useCallback等Api,但我們控制變量的值用useState 有可能會(huì)導(dǎo)致拿到的是舊值,并且如果他們更新會(huì)帶來(lái)整個(gè)組件重新執(zhí)行,這種情況下,我們使用useRef將會(huì)是一個(gè)非常不錯(cuò)的選擇
在react-redux的源碼中,在hooks推出后,react-redux用大量的useMemo重做了Provide等核心模塊,其中就是運(yùn)用useRef來(lái)緩存數(shù)據(jù),并且所運(yùn)用的 useRef() 沒(méi)有一個(gè)是綁定在dom元素上的,都是做數(shù)據(jù)緩存用的
可以簡(jiǎn)單的來(lái)看一下:
// 緩存數(shù)據(jù)
/* react-redux 用userRef 來(lái)緩存 merge之后的 props */
const lastChildProps = useRef()
// lastWrapperProps 用 useRef 來(lái)存放組件真正的 props信息
const lastWrapperProps = useRef(wrapperProps)
//是否儲(chǔ)存props是否處于正在更新?tīng)顟B(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ù)源,減少了不必要的更新,如過(guò)采取useState勢(shì)必會(huì)重新渲染
useLatest
經(jīng)過(guò)上面的講解我們知道useRef 可以拿到最新值,我們可以進(jìn)行簡(jiǎn)單的封裝,這樣做的好處是:可以隨時(shí)確保獲取的是最新值,并且也可以解決閉包問(wèn)題
import { useRef } from 'react';
const useLatest = <T>(value: T) => {
const ref = useRef(value)
ref.current = value
return ref
};
export default useLatest;
結(jié)合useMemo和useRef封裝useCreation
useCreation :是 useMemo 或 useRef的替代品。換言之,useCreation這個(gè)鉤子增強(qiáng)了 useMemo 和 useRef,讓這個(gè)鉤子可以替換這兩個(gè)鉤子。(來(lái)自ahooks-useCreation[2])
useMemo的值不一定是最新的值,但useCreation可以保證拿到的值一定是最新的值對(duì)于復(fù)雜常量的創(chuàng)建, useRef容易出現(xiàn)潛在的的性能隱患,但useCreation可以避免
這里的性能隱患是指:
// 每次重渲染,都會(huì)執(zhí)行實(shí)例化 Subject 的過(guò)程,即便這個(gè)實(shí)例立刻就被扔掉了
const a = useRef(new Subject())
// 通過(guò) factory 函數(shù),可以避免性能隱患
const b = useCreation(() => new Subject(), [])
接下來(lái)我們來(lái)看看如何封裝一個(gè)useCreation,首先我們要明白以下三點(diǎn):
第一點(diǎn):先確定參數(shù), useCreation的參數(shù)與useMemo的一致,第一個(gè)參數(shù)是函數(shù),第二個(gè)參數(shù)參數(shù)是可變的數(shù)組第二點(diǎn):我們的值要保存在 useRef中,這樣可以將值緩存,從而減少無(wú)關(guān)的刷新第三點(diǎn):更新值的判斷,怎么通過(guò)第二個(gè)參數(shù)來(lái)判斷是否更新 useRef里的值。
明白了一上三點(diǎn)我們就可以自己實(shí)現(xiàn)一個(gè)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++) {
// 判斷兩個(gè)值是否是同一個(gè)值
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判斷是否更新值通過(guò)initialized 和 depsAreSame來(lái)判斷,其中depsAreSame通過(guò)存儲(chǔ)在 useRef下的deps(舊值) 和 新傳入的 deps(新值)來(lái)做對(duì)比,判斷兩數(shù)組的數(shù)據(jù)是否一致,來(lái)確定是否更新
驗(yàn)證 useCreation
接下來(lái)我們寫(xiě)個(gè)小例子,來(lái)驗(yàn)證下 useCreation是否能滿(mǎn)足我們的要求:
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;
我們可以看到,當(dāng)我們做無(wú)關(guān)的state改變的時(shí)候,正常的函數(shù)也會(huì)刷新,但useCreation沒(méi)有刷新,從而增強(qiáng)了渲染的性能~
useEffect
useEffect相信各位小伙伴已經(jīng)用的熟的不能再熟了,我們可以使用useEffect來(lái)模擬下class的componentDidMount和componentWillUnmount的功能。
useMount
這個(gè)鉤子不必多說(shuō),只是簡(jiǎn)化了使用useEffect的第二個(gè)參數(shù):
import { useEffect } from 'react';
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.();
}, []);
};
export default useMount;
useUnmount
這個(gè)需要注意一個(gè)點(diǎn),就是使用useRef來(lái)確保所傳入的函數(shù)為最新的狀態(tài),所以可以結(jié)合上述講的useLatest結(jié)合使用
import { useEffect, useRef } from 'react';
const useUnmount = (fn: () => void) => {
const ref = useRef(fn);
ref.current = fn;
useEffect(
() => () => {
fn?.()
},
[],
);
};
export default useUnmount;
結(jié)合useMount和useUnmount做個(gè)小例子
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:強(qiáng)制更新
有的時(shí)候我們需要組件強(qiáng)制更新,這個(gè)時(shí)候就可以使用這個(gè)鉤子:
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>時(shí)間:{Date.now()}</div>
<Button color='primary' onClick={update}>更新時(shí)間</Button>
</div>
);
}
export default Index;
效果如下:

案例
案例1: useReactive
useReactiv: 一種具備響應(yīng)式的useState
緣由:我們知道用useState可以定義變量其格式為:
const [count, setCount] = useState<number>(0)
通過(guò)setCount來(lái)設(shè)置,count來(lái)獲取,使用這種方式才能夠渲染視圖
來(lái)看看正常的操作,像這樣 let count = 0; count =7 此時(shí)count的值就是7,也就是說(shuō)數(shù)據(jù)是響應(yīng)式的
那么我們可不可以將 useState也寫(xiě)成響應(yīng)式的呢?我可以自由設(shè)置count的值,并且可以隨時(shí)獲取到count的最新值,而不是通過(guò)setCount來(lái)設(shè)置。
我們來(lái)想想怎么去實(shí)現(xiàn)一個(gè)具備 響應(yīng)式 特點(diǎn)的 useState 也就是 useRective,提出以下疑問(wèn),感興趣的,可以先自行思考一下:
這個(gè)鉤子的出入?yún)⒃撛趺丛O(shè)定? 如何將數(shù)據(jù)制作成響應(yīng)式(畢竟普通的操作無(wú)法刷新視圖)? 如何使用 TS去寫(xiě),完善其類(lèi)型?如何更好的去優(yōu)化?
分析
以上四個(gè)小問(wèn)題,最關(guān)鍵的就是第二個(gè),我們?nèi)绾螌?shù)據(jù)弄成響應(yīng)式,想要弄成響應(yīng)式,就必須監(jiān)聽(tīng)到值的變化,在做出更改,也就是說(shuō),我們對(duì)這個(gè)數(shù)進(jìn)行操作的時(shí)候,要進(jìn)行相應(yīng)的攔截,這時(shí)就需要ES6的一個(gè)知識(shí)點(diǎn):Proxy
在這里會(huì)用到 Proxy和Reflect的點(diǎn),感興趣的可以看看我的這篇文章:??花一個(gè)小時(shí),迅速了解ES6\~ES12的全部特性[3]
Proxy:接受的參數(shù)是對(duì)象,所以第一個(gè)問(wèn)題也解決了,入?yún)⒕蜑閷?duì)象。那么如何去刷新視圖呢?這里就使用上述的useUpdate來(lái)強(qiáng)制刷新,使數(shù)據(jù)更改。
至于優(yōu)化這一塊,使用上文說(shuō)的useCreation就好,再配合useRef來(lái)放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;
這里先說(shuō)下TS,因?yàn)槲覀儾恢罆?huì)傳遞什么類(lèi)型的initialState所以在這需要使用泛型,我們接受的參數(shù)是對(duì)象,可就是 key-value 的形式,其中 key 為 string,value 可以是 任意類(lèi)型,所以我們使用 Record<string, any>
有不熟悉的小伙伴可以看看我的這篇文章:一篇讓你完全夠用TS的指南[4](又推銷(xiāo)一遍,有點(diǎn)打廣告,別在意~)
再來(lái)說(shuō)下攔截這塊,我們只需要攔截設(shè)置(set) 和 獲取(get) 即可,其中:
設(shè)置這塊,需要改變是圖,也就是說(shuō)需要,使用useUpdate來(lái)強(qiáng)制刷新 獲取這塊,需要判斷其是否為對(duì)象,是的話(huà)繼續(xù)遞歸,不是的話(huà)返回就行
驗(yàn)證
接下來(lái)我們來(lái)驗(yàn)證一下我們寫(xiě)的 useReactive,我們將以 字符串、數(shù)字、布爾、數(shù)組、函數(shù)、計(jì)算屬性幾個(gè)方面去驗(yàn)證一下:
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}}> 對(duì)數(shù)字進(jìn)行操作:{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 } >設(shè)置為7</Button>
</div>
<div style={{marginTop: 8}}> 對(duì)字符串進(jìn)行操作:{state.name}</div>
<div style={{margin: '8px 0', display: 'flex',justifyContent: 'flex-start'}}>
<Button color='primary' onClick={() => state.name = '小杜杜' } >設(shè)置為小杜杜</Button>
<Button color='primary' style={{marginLeft: 8}} onClick={() => state.name = 'Domesy'} >設(shè)置為Domesy</Button>
</div>
<div style={{marginTop: 8}}> 對(duì)布爾值進(jìn)行操作:{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}}> 對(duì)數(shù)組進(jìn)行操作:{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}}>計(jì)算屬性:</div>
<div style={{marginTop: 8}}>數(shù)量:{ state.bugsCount } 個(gè)</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)聽(tīng)各種事件的時(shí)候需要做監(jiān)聽(tīng),如:監(jiān)聽(tīng)點(diǎn)擊事件、鍵盤(pán)事件、滾動(dòng)事件等,我們將其統(tǒng)一封裝起來(lái),方便后續(xù)調(diào)用
說(shuō)白了就是在addEventListener的基礎(chǔ)上進(jìn)行封裝,我們先來(lái)想想在此基礎(chǔ)上需要什么?
首先,useEventListener的入?yún)⒖煞譃槿齻€(gè)
第一個(gè) event是事件(如:click、keydown)第二個(gè)回調(diào)函數(shù)(所以不需要出參) 第三個(gè)就是目標(biāo)(是某個(gè)節(jié)點(diǎn)還是全局)
在這里需要注意一點(diǎn)就是在銷(xiāo)毀的時(shí)候需要移除對(duì)應(yīng)的監(jiān)聽(tīng)事件
代碼
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默認(rèn)設(shè)置成了window,至于為什么要這么寫(xiě):'current' in target是因?yàn)槲覀冇?code style="font-size: 14px;word-wrap: break-word;border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: #9b6e23;background-color: #fff5e3;padding: 3px;margin: 3px;">useRef拿到的值都是 ref.current
優(yōu)化
接下來(lái)我們一起來(lái)看看如何優(yōu)化這個(gè)組件,這里的優(yōu)化與 useCreation 類(lèi)似,但又有不同,原因是這里的需要判斷的要比useCreation復(fù)雜一點(diǎn)。
再次強(qiáng)調(diào)一下,傳遞過(guò)來(lái)的值,優(yōu)先考慮使用
useRef,再考慮用useState,可以直接使用useLatest,防止拿到的值不是最新值
這里簡(jiǎn)單說(shuō)一下我的思路(又不對(duì)的地方或者有更好的建議歡迎評(píng)論區(qū)指出):
首先需要 hasInitRef來(lái)存儲(chǔ)是否是第一次進(jìn)入,通過(guò)它來(lái)判斷初始化存儲(chǔ)然后考慮有幾個(gè)參數(shù)需要存儲(chǔ),從上述代碼上來(lái)看,可變的變量有兩個(gè),一個(gè)是 event,另一個(gè)是target,其次,我們還需要存儲(chǔ)對(duì)應(yīng)的卸載后的函數(shù),所以存儲(chǔ)的變量應(yīng)該有3個(gè)接下來(lái)考慮一下什么情況下觸發(fā)更新,也就是可變的兩個(gè)參數(shù): event和target最后在卸載的時(shí)候可以考慮使用 useUnmount,并執(zhí)行存儲(chǔ)對(duì)應(yīng)的卸載后的函數(shù)和把hasInitRef還原
詳細(xì)代碼
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); // 一開(kāi)始設(shè)置初始化
const elementRef = useRef<(Element | null)[]>([]);// 存儲(chǔ)具體的值
const depsRef = useRef<DependencyList>([]); // 存儲(chǔ)傳遞的deps
const unmountRef = useRef<any>(); // 存儲(chǔ)對(duì)應(yīng)的effect
// 初始化 組件的初始化和更新都會(huì)執(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
}
// 校驗(yàn)變值: 目標(biāo)的值不同, 依賴(lài)值改變
if(elementRef.current !== targetElement || !depsAreSame(deps, depsRef.current)){
//先執(zhí)行對(duì)應(yīng)的函數(shù)
unmountRef.current?.();
//重新進(jìn)行賦值
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;
// 防止沒(méi)有 addEventListener 這個(gè)屬性
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是因?yàn)椋诟潞统跏蓟那闆r下都需要使用必須要防止沒(méi)有 addEventListener這個(gè)屬性的情況,監(jiān)聽(tīng)的目標(biāo)有可能沒(méi)有加載出來(lái)
驗(yàn)證
驗(yàn)證一下useEventListener是否能夠正常的使用,順變驗(yàn)證一下初始化、卸載的,代碼:
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)聽(tīng)鍵盤(pán)事件:{key}</div>
</div>
}
</div>
);
}
export default Index;
效果:

我們可以利用useEventListener這個(gè)鉤子去封裝其他鉤子,如 鼠標(biāo)懸停,長(zhǎng)按事件,鼠標(biāo)位置等,在這里在舉一個(gè)鼠標(biāo)懸停的小例子
小例子 useHover
useHover:監(jiān)聽(tīng) DOM 元素是否有鼠標(biāo)懸停
這個(gè)就很簡(jiǎn)單了,只需要通過(guò) useEventListener來(lái)監(jiān)聽(tīng)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: 有關(guān)時(shí)間的Hooks
在這里主要介紹有關(guān)時(shí)間的三個(gè)hooks,分別是:useTimeout、useInterval和useCountDown
useTimeout
useTimeout:一段時(shí)間內(nèi),執(zhí)行一次
傳遞參數(shù)只要函數(shù)和延遲時(shí)間即可,需要注意的是卸載的時(shí)候?qū)⒍〞r(shí)器清除下就OK了
詳細(xì)代碼:
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: 每過(guò)一段時(shí)間內(nèi)一直執(zhí)行
大體上與useTimeout一樣,多了一個(gè)是否要首次渲染的參數(shù)immediate
詳細(xì)代碼:
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:簡(jiǎn)單控制倒計(jì)時(shí)的鉤子
跟之前一樣我們先來(lái)想想這個(gè)鉤子需要什么:
我們要做倒計(jì)時(shí)的鉤子首先需要一個(gè)目標(biāo)時(shí)間(targetDate),控制時(shí)間變化的秒數(shù)(interval默認(rèn)為1s),然后就是倒計(jì)時(shí)完成后所觸發(fā)的函數(shù)(onEnd) 返參就更加一目了然了,返回的是兩個(gè)時(shí)間差的數(shù)值(time),再詳細(xì)點(diǎn)可以換算成對(duì)應(yīng)的天、時(shí)、分等(formattedRes)
詳細(xì)代碼
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(); //計(jì)算差值
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;
驗(yàn)證
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('結(jié)束')
},
});
return (
<div style={{padding: 20}}>
<div> 距離 2022-12-31 24:00:00 還有 {days} 天 {hours} 時(shí) {minutes} 分 {seconds} 秒 {milliseconds} 毫秒</div>
<div>
<p style={{marginTop: 12}}>動(dòng)態(tài)變化:</p>
<Button color='primary' disabled={countdown !== 0} onClick={() => setCount(Date.now() + 3000)}>
{countdown === 0 ? '開(kāi)始' : `還有 ${Math.round(countdown / 1000)}s`}
</Button>
<Button style={{marginLeft: 8}} onClick={() => setCount(undefined)}>停止</Button>
</div>
</div>
);
}
export default Index;
效果展示:

End
參考
ahooks[5]
總結(jié)
簡(jiǎn)單的做下總結(jié):
一個(gè)優(yōu)秀的hooks一定會(huì)具備 useMemo、useCallback等api優(yōu)化制作自定義hooks遇到傳遞過(guò)來(lái)的值,優(yōu)先考慮使用 useRef,再考慮用useState,可以直接使用useLatest,防止拿到的值不是最新值在封裝的時(shí)候,應(yīng)該將存放的值放入 useRef中,通過(guò)一個(gè)狀態(tài)去設(shè)置他的初始化,在判斷什么情況下來(lái)更新所對(duì)應(yīng)的值,明確入?yún)⑴c出參的具體意義,如useCreation和useEventListener
盤(pán)點(diǎn)
本文一共講解了12個(gè)自定義hooks,分別是:usePow、useLatest、useCreation、useMount、useUnmount、useUpdate、useReactive、useEventListener、useHover、useTimeout、useInterval、useCountDown
這里的素材來(lái)源為ahooks,但與ahooks的不是完全一樣,有興趣的小伙伴可以結(jié)合ahooks源碼對(duì)比來(lái)看,自己動(dòng)手敲敲,加深理解
相信在這篇文章的幫助下,各位小伙伴應(yīng)該跟我一樣對(duì)Hooks有了更深的理解,當(dāng)然,實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn),多多敲代碼才是王道~
另外,覺(jué)得這篇文章能夠幫助到你的話(huà),請(qǐng)點(diǎn)贊+收藏一下吧,順便關(guān)注下專(zhuān)欄,之后會(huì)輸出有關(guān)React的好文,一起上車(chē)學(xué)習(xí)吧~
react其他好文:「React深入」這就是HOC,這次我終于悟了!!![6]
關(guān)于本文
來(lái)自:小杜杜
https://juejin.cn/post/7101486767336849421

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專(zhuān)業(yè)的技術(shù)人...


參考資料
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
