18个好用的自定义react hook

我心飞翔 分类:javascript

这是我阅读ahooks源码后,写下的笔记,如果有错漏的地方还请指正。
搭配ahooks官方文档:ahooks.js.org/ 和ahooks的github仓库:github.com/alibaba/hoo… 阅读为佳

1. useCreation

useCreation是useMemo和useRef的替代品,性能更好

function fn(){
  const a = useRef(new Subject())// fn每次重新渲染都会创建Subject实例
  // 无论fn重新渲染几次,useCreation都会去判断依赖是否改变,
  //再决定是否执行factory函数(第一个参数)
  const a = useCreation(()=>new Subject(),[deps])
}
 

实现useCreation 

  1. 确定输入输出,useCreation接受两个参数,一个工厂函数,一个依赖项数组,并返回工厂函数执行后的结果
//  使用泛型T约束了useCreation返回的结果必须与工厂函数返回的内容一致
function useCreation<T>(factory:()=>T,deps:DependencyList[]):T;
 
  1. 分析,组件重新渲染时,需要判断依赖项是否变化而重新执行factory函数,则我们可以知道依赖项和factory返回的内容需要持久化。factory函数只有在依赖项变化和首次渲染时执行,则还需要知道useCreation是否已经初始化过
function useCreation<T>(factory:()=>T,deps:DependencyList[]):T{
    const {current}=useRef({
    obj:undefined as undefined | T,/ factory返回的内容存储在obj中
    deps,// 依赖项
    initialized:false//是否初始化
  })
}
 
  1. 判断依赖项是否相同
function depsAreSame(oldDeps:any[],deps:DependencyList[]):boolean{
	if(oldDeps===deps){
  	return true;
  }
  for(const i in oldDeps){
  	if(oldDeps[i]!==deps[i]){
    	return false
    }
  }
  return true;
}
 
  1. 初始化及依赖项改变时,才执行factory
if(!current.initialized||!depsAreSame(current.deps,deps)){
  current.obj = factory()
  current.deps=deps;
  current.initialized=true
}
 
  1. 完整代码
function useCreation<T>(factory:()=>T,deps:any[]):T{
  const {current} = useRef({
    obj:undefined as undefined | T,
    initialized:false,
    deps,
  })
  
  if(!current.initialized||depsAreSame(current.deps,deps)){
    current.obj = factory()
    current.initialized=true;
    current.deps = deps;
  }
  return current.obj as T
}

function depsAreSame(oldDeps:any[],deps:any[]):boolean{
	if(oldDeps===deps){
  	return true;
  }
  for(const i in oldDeps){
  	if(oldDeps[i]!==deps[i]){
    	return false;
    }
  }
  return true;
}
 

2. useDebounceFn

用来函数防抖的hook

函数防抖类似电梯门的开关,电梯门正常会等待10s后关闭,但如果你在关闭前又触发了电梯门的开关机制,那电梯门就会刷新等待时间,重新等待10秒后关闭
实现 

  1. 第一版
type Fn = (...args: any) => any
export default function DebounceFn<T extends Fn>(func: T, wait: number) {
    let timeout: NodeJS.Timeout;
    return function () {
        if (timeout) {
            clearTimeout(timeout)
        }
        timeout = setTimeout(func, wait)
    }
}
 

第一版比较简陋,可以发现,缺少了this的指向,参数的传递,函数的返回值,我们其实应该保证,返回的函数要和传入的func一致,毕竟函数防抖只是改变函数的执行时机,但不应该改变函数的参数和内部的实现机制

  1. 第二版
// 返回函数的参数类型
type ArgumentsTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;

// 定义传入的函数类型
type Fn = (...args: any) => any

//防抖后返回的函数类型
type ReturnFn<K extends Fn> = (...args: ArgumentsTypes<K>) => ReturnType<K>


export default function DebounceFn<K extends Fn>(fn: K, wait: number): ReturnFn<K> {
    let timeout: NodeJS.Timeout
    // ReturnType<K> 定义函数的返回值类型
    let result: ReturnType<K>
    return function (this: any, ...args: ArgumentsTypes<K>) {
        if (timeout) {
            clearTimeout(timeout)
        }
        timeout = setTimeout(() => {
          	// 解决了this的指向问题,和参数的传递
            result = fn.apply(this, args)
        }, wait)

      	// 返回了函数的返回值
        return result;
    }
}
 

image.png
image.png
这一版,我们解决了this指向问题,参数传递问题,函数的返回值问题,并且借助TS完成了对函数的类型推导。
现在我们要增加一个功能,即每次触发事件时,防抖函数根据immediate来判断是否立即执行
image.png
如果immediate是true,则在wait的开头去执行,并且wait期间不再执行

  1. 第三版
type ArgumentsTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;
type Fn = (...args: any) => any
type ReturnFn<K extends Fn> = (...args: ArgumentsTypes<K>) => ReturnType<K>
export default function DebounceFn<K extends Fn>(fn: K, wait: number, immediate: boolean): ReturnFn<K> {
let timeout: NodeJS.Timeout | null
let result: ReturnType<K>
return function (this: any, ...args: ArgumentsTypes<K>) {
const later = () => {
// wait结束后,timeout赋值为null
// 标志另一个wait的开头
timeout = null
// wait结束后, immediate:false,执行函数
if (!immediate) {
result = fn.apply(this, args)
}
}
// immediate:true 函数要立即执行 
if (immediate) {
// 没有定时器即代表wait的开头
if (!timeout) {
// 给timeout赋值,表明不在wait的开头,进入wait内
timeout = setTimeout(() => {
later()
}, wait)
result = fn.apply(this, args)
}
} else {
// immediate:false
// 每次触发,则清除之前的定时器,开始新的定时器
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
later()
}, wait)
}
return result;
}
}
  1. 第四版,添加一个取消当前防抖的功能
export default function DebounceFn<K extends Fn>
(fn: K, wait: number, immediate: boolean): ReturnFn<K> & { cancel: () => void } 
{
let timeout: NodeJS.Timeout | null
let result: ReturnType<K>
function _debounce(this: any, ...args: ArgumentsTypes<K>) {
const later = () => {
// wait结束后,timeout赋值为null
// 标志另一个wait的开头
timeout = null
// wait结束后, immediate:false的执行函数
if (!immediate) {
result = fn.apply(this, args)
}
}
// immediate:true 函数要立即执行 
if (immediate) {
// 没有定时器即代表wait的开头
if (!timeout) {
// 给timeout赋值,表明不在wait的开头,进入wait内
timeout = setTimeout(() => {
later()
}, wait)
result = fn.apply(this, args)
}
} else {
// immediate:false
// 每次触发,则清除之前的定时器,开始新的定时器
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
later()
}, wait)
}
return result;
}
_debounce.cancel = function () {
if (timeout) {
clearTimeout(timeout)
}
}
return _debounce
}

函数防抖已经做好了,但是在函数组件中使用,每次渲染就会重新生成一个防抖处理的函数,太耗性能,我们使用hook将生成的防抖函数地址固定下。

  1. 第五版,配合hook
export function useDebounceFn<T extends Fn>(fn: T, wait: number, immediate: boolean) {
const fnRef = useRef<T>(fn)
fnRef.current = fn
const debounce = useCreation(() => {
return DebounceFn(fnRef.current, wait, immediate)
}, [])
return {
run: debounce as any as T,
cancel: debounce.cancel
}
}

image.png
image.png

3.useDebounce

用来处理防抖值的hook
useDebounceFn是对函数进行防抖,useDebounce是对值进行防抖

实现 

  1. 确定输入,输出。既是对值进行防抖,则输入是value,wait,immediate,输出则是value
export default function useDebounceFn<T>(value: T, wait: number, immediate: boolean):T {}
  1. 内部声明一个state来存储防抖处理的值
export default function useDebounce<T>(value: T, wait: number, immediate: boolean): T {
const [state, setState] = useState<T>(value)
const { run } = useDebounceFn(() => {
setState(value)
}, wait, immediate)
return state;
}
  1. 监听外部value的变化,并去调用run
export default function useDebounce<T>(value: T, wait: number, immediate: boolean): T {
const [state, setState] = useState<T>(value)
const { run } = useDebounceFn(() => {
setState(value)
}, wait, immediate)
useEffect(() => {
run()
}, [value])
return state;
}

4. useInterval

一个可以处理setInterval的hook

export default function(){
const [num,setNum] = useState(0)
useEffect(()=>{
setInterval(()=>{
setNum(num+1)
},1000)
},[])
}

上面的代码中,原本是想每过一秒钟num便增加1,但实际运行时,不论过多少秒,num只会增加到1便停止了。
这是因为在setInterval中用的num,是最初始的上下文中的num=0,于是便会一直重复setNum(0+1)
为了正常使用setInterval,我们只需要在组件重新渲染时,给setInterval传入最新的执行函数即可
实现 

  1. 确定输入,输出,输入:一个需要执行的函数fn,定时器的时间wait,是否立刻执行immediate,不输出
interface IOptions {
immediate: boolean
}
export default function useInterval(fn: () => void, wait: number, { immediate }: IOptions):void;
  1. 为了在setInterval中执行最新的函数,我们需要使用useRef。并且setInterval一般都是在组件渲染后才执行的,所以我们需要useEffect
export default function useInterval(fn: () => void, wait: number, { immediate }: IOptions) {
const fnRef = useRef(fn)
fnRef.current = fn
useEffect(() => {
setInterval(fnRef.current, wait)
}, [wait])
}
  1. 加上组件卸载时清除定时器和立刻执行
export default function useInterval(fn: () => void, wait: number, { immediate }: IOptions) {
const fnRef = useRef(fn)
fnRef.current = fn
let timer: NodeJS.Timeout
useEffect(() => {
// immediate:true表示是要立刻执行
if (immediate) {
fnRef.current()
}
timer = setInterval(fnRef.current, wait)
// 组件卸载时别忘了清除定时器
return () => {
clearTimeout(timer)
}
}, [wait])
}

5.useEventEmitter

在多个组件之间进行事件通知有时会让人非常头疼,借助 EventEmitter ,可以让这一过程变得更加简单。

EventEmitter一般都是用类来实现,内部有三个属性方法,一个属性存储订阅的事件,一个方法订阅事件,一个方法触发事件
实现: 

// 定义订阅的事件类型
type SubScription<T> = (val: T) => void
class EventEmitter<T>{
// 定义一个私有属性,用于存储订阅事件
// set可以保证不会重复订阅重复事件
private subscriptions = new Set<SubScription<T>>()
// 订阅事件
useSubScription = (callback: SubScription<T>) => {
// 使用ref可以保证执行事件时,函数是最新的,
// useEffect的依赖项为空数组,使用ref,可以保证在useEffect中执行的事件是最新的
const callbackRef = useRef<SubScription<T>>()
callbackRef.current = callback
useEffect(() => {
// 增加一层判断,订阅事件的函数存在时,才执行
function subscription(val: T) {
if (callbackRef.current) {
callbackRef.current(val)
}
}
// 订阅事件
this.subscriptions.add(subscription)
// 组件销毁时,删除订阅事件
return () => {
this.subscriptions.delete(subscription)
}
// 不论组件如何渲染,注册事件,只执行一次
}, [])
}
// 触发事件
// 注意T
// 事件的参数类型是T,与useSubScription订阅的函数的参数类型一致
emit = (val: T) => {
// 遍历事件
for (const subscription of this.subscriptions) {
subscription(val)
}
}
}
// 因为EventEmitter是个类,函数组件每次渲染,都会生成一个新的对象,
// 所以需要使用下ref
export default function useEventEmitter<T>() {
const eventEmitterRef = useRef<EventEmitter<T>>()
if (!eventEmitterRef.current) {
eventEmitterRef.current = new EventEmitter()
}
return eventEmitterRef.current
}

6. useLock

用于给一个异步函数增加竞态锁,防止并发执行。

实现这个,只需要使用ref来存储锁的开关,函数开始执行时,锁关上,执行完毕后,锁打开。锁是关的状态不会触发函数执行

export function useLockFn<T extends any[], K>(fn: (...args: T) => Promise<K>): (...args: T) => Promise<K | undefined> {
const lockRef = useRef(false)
return useCallback(async (...args: T) => {
if (lockRef.current) return
try {
lockRef.current = true;
const result = await fn(...args)
return result
} catch (e) {
throw e
} finally {
lockRef.current = false
}
}, [fn])
}

7.useReactive

提供一种数据响应式的操作体验,定义数据状态不需要写useState , 直接修改属性即可刷新视图。

  1. 了解下背景知识 Reflect.get(target, name, receiver) 

Reflect.get方法查找并返回target对象的name属性,如果没有该属性,则返回undefined

var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}
Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3

如果name属性部署了读取函数(getter),则读取函数的this绑定receiver

var myObject = {
foo: 1,
bar: 2,
get baz() {
// 这里的this指代,Reflect.get中的receiver
return this.foo + this.bar;
},
};
var myReceiverObject = {
foo: 4,
bar: 4,
};
Reflect.get(myObject, 'baz', myReceiverObject) // 8
  1. 为了实现数据响应式,我们需要使用Proxy和Reflect创建一个观察者,对数据进行代理
function Observer<T extends Object>(initState:T,cb:()=>void):T{
const proxy = New Proxy<T>(initState,{
get(target,prop,receiver){
return Reflect.get(target,prop,receiver)
},
set(target,prop,value){
const ret = Reflect.set(target,prop,value)
// 每次赋值都要调用回调函数
cb();
return ret;
},
deleteProperty(target,key){
const ret = Reflect.deleteProperty(target,key)
cb();
return ret;
}
})
return proxy;
}
  1. 数据可能是一个多层级的对象,所以需要对数据进行递归代理
function isObject<T extends Object>(val:T):boolean{
return typeof val==="Object"&&val!==null
}
function Observer<T extends Object>(initState:T,cb:()=>void):T{
const proxy = New Proxy<T>(initState,{
get(target,prop,receiver){
// 判断代理的属性是不是一个对象,是则递归代理
// receiver指代proxy实例,当时获取的属性是个函数,且函数内使用了this时,this指代receiver
const ret = Reflect.get(target,prop,receiver)
return isObject(ret)?Observer(ret,cb):Reflect.get(target,prop)
},
set(target,prop,value){
const ret = Reflect.set(target,prop,value)
// 每次赋值都要调用回调函数
cb();
return ret;
},
deleteProperty(target,key){
const ret = Reflect.deleteProperty(target,key)
cb();
return ret;
}
})
return proxy;
}
  1. 上面只是对数据进行了代理,但是数据即使变化了,react组件也不会重新渲染,所以预留了cb函数,当数据变化时,刷新组件
export default function useReactive<S extends Object>(state:S):S{
// 强制刷新 
const [,forceUpdate] = useState({})
// 每次函数组件重新渲染执行时,都会传入新的state,这样导致每次都是对新的state进行代理
// 所以需要持久化下state
const stateRef = useRef(state)
return useMemo(()=>Observer(stateRef.current,()=>{
// 每次数据进行了赋值操作,则强制刷新组件
forceUpdate()
}),[])
}
  1. 优化,需要防止重复代理,以及防止代理已经代理过的对象
// k:v 原对象:代理过的对象
const proxyMap = new WeakMap();
// k:v 代理过的对象:原对象
const rawMap = new WeakMap();
function Observer<T extends Object>(initState: T, cb: () => void): T {
const existingProxy = proxyMap.get(initState)
// 已经代理过,则不在重复代理
if (existingProxy) {
return existingProxy
}
// 防止代理已经代理过的对象
if (rawMap.has(initState)) {
return initState
}
const proxy = new Proxy<T>(initState, {
get(target, prop, receiver) {
// 判断代理的属性是不是一个对象,是则递归代理
// receiver指代proxy实例,当时获取的属性是个函数,且函数内使用了this时,this指代receiver
const ret = Reflect.get(target, prop, receiver)
return isObject(ret) ? Observer(ret, cb) : Reflect.get(target, prop)
},
set(target, prop, value) {
const ret = Reflect.set(target, prop, value)
// 每次赋值都要调用回调函数
cb();
return ret;
},
deleteProperty(target,key){
const ret = Reflect.deleteProperty(target,key)
cb();
return ret;
}
})
proxyMap.set(initState,proxy)
rawMap.set(proxy,initState)
return proxy;
}

8. useTrackedEffect

在 useEffect 的基础上,追踪触发 effect 的依赖变化。

实现 

  1. 确定输入输出,
// changeIndex变化的哪个依赖的索引
type Effect = (changeIndex: number[], previousDeps: DependencyList | undefined, currentDeps: DependencyList) => any
// effect:副作用函数
// deps:依赖项数组
export default function useTrackedEffect(effect: Effect, deps: DependencyList) {
  1. 想知道改变的是依赖项数组中的哪一个,我们需要写个函数来判断
const diffTwoDeps = (preDeps:DependencyList|undefined,curDeps:DependencyList)=>{
// 组件初始化时,pre显然是不存在的,
return preDeps
? preDeps.map((item,index)=>curDeps[index]!==item?index:-1).filter(item=>item>0)
: curDeps
? curDeps.map((item,index)=>index)
:[]
}
  1. useTrackedEffect本质还是useEffect,并且为了对比前后deps的变化,还需要借助ref
export default function useTrackedEffect(effect:Effect,deps:DependencyList){
// 为了对比依赖项的变化,必须持久化依赖项,以便对比
const previousDepsRef = useRef<DependencyList>()
useEffect(()=>{
const changeIndex = diffTwoDeps(previousDepsRef.current,deps)
const previousDeps = previousDepsRef.current
previousDepsRef.current = deps
return effect(changeIndex,previousDeps,deps)
},deps)
}

9.useUpdateEffect

一个只在依赖更新时执行的 useEffect hook。

import { useEffect, useRef, DependencyList, EffectCallback } from "react";
export default function useUpdateEffect(effect: EffectCallback, deps: DependencyList) {
const isMount = useRef(true);
useEffect(() => {
if (!isMount.current) {
// 记得要return 
return effect()
} else {
isMount.current = false
}
}, deps)
}

10.useControllableValue

在某些组件开发时,我们需要组件的状态即可以自己管理,也可以被外部控制,useControllableValue 就是帮你管理这种状态的 Hook。

实现:

  1. 使用:
const ControllableComponent = (props: any) => {
const [state, setState] = useControllableValue<string>(props);
}
  1. 确定输入输出:
// 父级组件传递过来的Props
interface Props {
[key: string]: any
}
interface IOptions<T> {
defaultValue?: T //组件自身的默认值
valuePropName?: string // 定义父级组件传递的值的属性名
defaultPropName?: string // 父级组件传递的默认值的属性名
trigger?: string // 修改值时,触发的父级组件传递过来的函数,
}
export default function useControllableValue<T>(props: Props, options: IOptions<T>) {
const {
defaultValue,
defaultPropName = "defaultValue",
valuePropName = "value",
trigger = "onChange"
} = options
}
  1. 分析:
    1. 状态既可以由父级组件控制,也可以由组件自身控制
    2. 由此可得,需要拿到父级组件传入的props
    3. 父组件需要完全控制value,那value的属性名是什么,valuePropName="value"
    4. 父组件只是传递一个默认值,那么默认值的属性名是什么,defaultPropName="defaultValue"
    5. 父组件需要知道值的变化,则需要执行回调函数,那么回调函数的属性名是什么:trigger="onChange"
    6. 组件自身需要默认值:defaultValue
  2. 状态的优先顺序是父组件传入的value>父组件传入的defaultValue>组件自身的默认值
export default function useControllableValue<T>(props:Props,options:IOptions<T>){
const {
defaultValue,
defaultPropName="defaultValue",
valuePropName="value",
trigger="onChange"
} = options
// 拿到父组件传入的值
const value = props[valuePropName]
const [state,setState] = useState(()=>{
// 父组件传入的默认值
if(defaultPropName in props){
return props[defaultPropName]
}
// 组件自身的默认值
return defaultValue
})
}
  1. 更新状态时,需要判断组件是受控组件还是非受控组件,受控组件则调用props.trigger,非受控组件则调用setState
const handleSetState = useCallback((e:T,...args:any[]){
// 如果valuePropName不存在,则组件是非受控组件
if(!props[valuePropName]){
setState(e)
}
// 如果trigger存在,则组件是受控组件
if(props[trigger]){
props[trigger](
e,
...args
)
}
},[valuePropName,trigger,props])
  1. 完整代码
interface Props {
[key: string]: any
}
interface IOptions<T> {
defaultValue?: T
valuePropName?: string
defaultPropName?: string
trigger?: string
}
export default function useControllableValue<T>(props: Props, options: IOptions<T>) {
const {
defaultValue,
defaultPropName = "defaultValue",
valuePropName = "value",
trigger = "onChange"
} = options
//  拿到父组件传入的值
const value = props[valuePropName]
const [state, setState] = useState<T | undefined>(() => {
// 父组件传入的默认值
if (defaultPropName in props) {
return props[defaultPropName]
}
//  组件自身的默认值
return defaultValue
})
const handleSetState = useCallback((e: T, ...args: any[]) => {
// 如果没有valuePropName 证明是非受控组件
if (!(valuePropName in props)) {
setState(e)
}
if (props[trigger]) {
props.trigger(e, ...args)
}
}, [trigger, props, valuePropName])
return [valuePropName in props ? value : state, handleSetState] as const
}

11. useMap

一个可以管理 Map 类型状态的 Hook。

import { useState, useMemo } from "react";
// 只要有Iterable接口就可以做map的参数
export default function useMap<K, T>(initState?: Iterable<readonly [K, T]>) {
// 保存默认值
const initMap = useMemo(() => {
return initState ? new Map(initState) : new Map()
}, [initState])
const [map, setMap] = useState<Map<K, T>>(initMap)
const stableActions = useMemo(() => ({
remove(key: K) {
setMap(pre => {
const map = new Map(pre)
map.delete(key)
return map;
})
},
setAll(state: Iterable<readonly [K, T]>) {
const newMap = new Map(state)
setMap(newMap)
},
set(key: K, value: T) {
setMap(pre => {
const map = new Map(pre)
map.set(key, value);
return map
})
},
reset() {
setMap(initMap)
}
}), [setMap, initMap])
const utils = {
get: (key: K) => map.get(key),
...stableActions
}
return [map, utils] as const
}

12. getTargetElement

可以拿到dom的方法

需求 

  1. 该方法可以接收一个函数,用于获取dom ()=>getElementsByClassName("abc") 
  2. 该方法可以接受一个dom,
  3. 该方法可以接受一个dom的ref

综上,可以定义一个基础类型

type BasicTarget<T=HTMLElement>= 
| (()=>T|null)// 一个函数执行后,返回一个dom|null
| T // dom
| null 
| MutableRefObject<T | null | undefined>// dom的ref

再完善些,T 不仅是HTMLElement,还可以是  | Element| Document | Window | HTMLElement

type TargetElement = | Element | Document | Window | HTMLElement

 实现 

  1. 确定输入输出:
export default function getTargetElement
//defaultTarget是在targetnull时,默认返回的
(target?:BasicTarget<TargetElement>,defaultTarget?:TargetElement)
// 函数最终返回
:TargetElement|null|undefined
 
  1. 判断target的类型,并作出相应的处理
export default function getTargetElement(target?:BasicTarget<TargetElement>,defaultTarget?:TargetElement):TargetElement|null|undefined{
// 如果target不存在,则返回默认dom
if(!target){
return defaultTarget
}
let targetElement:TargetElement|null|undefined
// 如果target是个函数,则执行该函数
if(typeof target === "function"){
targetElement = target()
//如果target是ref ,则返回ref.current
}else if ("current" in target){
targetElement = target.current
}else{
targetElement = target
}
return targetElement;
}

13. useClickAway

优雅的管理目标元素外点击事件的 Hook。

需求 :

  1. 触发目标区域外的dom事件时,触发回调函数
  2. 由上可得参数需要 回调函数 , dom , 事件 

实现: 

  1. 确定输入输出
// 定义默认事件 鼠标click
const defaultEvent = "click"
// 定义事件类型,浏览器的鼠标事件,移动端的触摸事件
type EventType = MouseEvent | TouchEvent
export default function useClickAway(
onClickAway:(e:EventType)=>void,
target:BasicTarget|BasicTarget[],// 目标dom,目标dom可以多个
eventName:string = defaultEvent// 监听的事件
)
 
  1. 如果需要监听目标dom区域外的事件,需要使用事件委托,在document上监听事件(注意需要在dom挂载后,再监听,需要使用useEffect)
export default function useClickAway(
onClickAway:(e:EventType)=>void,
target:BasicTarget|BasicTarget[],// 目标dom
eventName:string = defaultEvent// 监听的事件
){
const onClickAwayRef = useRef(onClickAway)
onClickAwayRef.current = onClickAway
useEffect(()=>{
const handler = ()=>{}
document.addEventListener(eventName,handler)
// 记得删除事件委托,避免内存泄漏
return ()=>{
document.removeEventListener(eventName,handler)
}
},[eventName,target])
}
  1. handler每次被调用,我们只需要判断事件源的dom 在不在目标dom中,在则不执行,不在则执行
const handler = (event:any)=>{
const targetArray = Array.isArray(target)?target:[target]
if(
targetArray.some(item=>{
// 拿到dom
const targetElement = getTargetElement(item) as HTMLElement;
// 目标dom不存在或者目标dom内含有触发事件的事件源的dom,则不执行
return !targetElement || targetElement.contains(event.target)})
){
return;
}
onClickAwayRef.current(event)
}

14.useSessionStorage

可以使用sessionStorage的hook

分析: 

  1. sessionStorage的改变不会使得react组件重新渲染,所以需要借助useState
  2. 什么时候使用sessionStorage?初始化时,将sessionStorage赋值给state
  3. 增删改查,sessionStorage和state同步即可,
  4. 返回state

实现: 

  1. 确定输入输出
export default useSessionStorage<T>(
key:string,
defaultValue?:T
):[state,updateState] as const
  1. 初始化时,使用sessionStorage给state赋值
const [state,setState] = useState<T|undefined>(()=>getStoreValue()) 
function getStoreValue(){
const raw = sessionStorage.getItem(key)
if(raw){
try{
return JSON.parse(raw)
}catch(err){}
}else{
return defaultValue
}
}
  1. 更新session
const updateState = useCallback((newState?:T)=>{
if(typeof newState === "undefined"){
sessionStorage.removeItem(key)
setState(undefined)
}else{
sessionStorage.setItem(kef,JSON.stringify(newState))
setState(newState)
}
},[key])
  1. useSessionStorageState 里也可以用 function updater,就像 useState 那样。
interface IFuncUpdater<T>{
(previousState?:T):T
}
// 为啥这里是obj is T  而不是boolean
// obj is T 成立时,obj便可以调用T类型中的方法与属性
// 而boolean则不行
function isFunction<T>(obj:any):obj is T{
return typeof obj==="fucntion"
}
const updateState = useCallback((value?:T|IFuncUpdater<T>)=>{
if(typeof newState === "undefined"){
sessionStorage.removeItem(key)
setState(undefined)
}esle if (isFunction<IFuncUpdater<T>>(value)){
const previousState = getStoreValue()
// 将上一次的value传入函数中
const newState = value(previousState)
sessionStorage.setItem(key,JSON.string(newState))
setState(newState)
}else{
sessionStorage.setItem(key,JSON.stringify(newState))
setState(newState)
}
},[key])

15 useEventListener

优雅使用 addEventListener 的 Hook。

实现 

  1. 确定输入输出

原生的addEventListener一般需要三个参数,绑定的事件名称,事件处理函数,目标dom,所以useEventListener同样需要这三个参数

type BasicTarget<T = HTMLElement> =
| T
| null
| (() => T | null)
| MutableRefObject<T | null | undefined>
export default function useEventListener(eventName: string, handler: Function, target: BasicTarget) { }

可以看到,虽然对有知道了输入输出,但是,对参数的类型限制太薄弱,eventName不能确保用户输入的事件类型名称是否正确,handler没有相应的传参类型提示

  1. 参数约束
import { MutableRefObject } from "react";
type BasicTarget<T = HTMLElement> =
| T
| null
| (() => T | null)
| MutableRefObject<T | null | undefined>;
type Target = BasicTarget<HTMLElement | Window | Document | Element>
function useEventListener<K extends keyof HTMLElementEventMap>(
eventName: K,
handler: (e: HTMLElementEventMap[K]) => void,
target: BasicTarget<HTMLElement>
): void;
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (e: WindowEventMap[K]) => void,
target: BasicTarget<Window>
): void;
function useEventListener<K extends keyof ElementEventMap>(
eventName: K,
handler: (e: ElementEventMap[K]) => void,
target: BasicTarget<Element>
): void;
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (e: DocumentEventMap[K]) => void,
target:BasicTarget<Document>
): void
function useEventListener(eventName: string, handler: Function, target: Target) { }
 

利用函数重载,我们对事件名,处理函数的参数,目标进行了限制

  1. 对目标进行事件绑定
function useEventListener(eventName: string, handler: Function, target: Target) {
/*
函数每次刷新,都会执行useEventListener,意味目标重复绑定事件,
使用useRef可以保证事件处理函数是最新的,
配合useEffect可以保证目标只绑定了一次事件函数
*/
const handlerRef = useRef(handler)
handlerRef.current = handler
useEffect(() => {
const targetElement = getTargetElement(target)!;
if (!targetElement.addEventListener) {
return
}
const eventListener = (e: Event): EventListenerOrEventListenerObject => {
return handlerRef.current && handlerRef.current(e)
}
targetElement.addEventListener(eventName, eventListener)
// 记得解除绑定,避免内存泄漏
return () => {
targetElement.removeEventListener(eventName, eventListener)
}
}, [])
}
  1. 增加选项,冒泡还是捕获?一次执行?是否执行默认事件?
interface IOptions<T extends Target = Target> {
target: T,
once?: boolean,// 是否只执行一次 false
capture?: boolean,// 是否在捕获阶段执行 false
passive?: boolean // 是否执行默认事件 false
}
function useEventListener<K extends keyof HTMLElementEventMap>(
eventName: K,
handler: (e: HTMLElementEventMap[K]) => void,
options: IOptions<HTMLElement>
): void;
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (e: WindowEventMap[K]) => void,
options: IOptions<Window>
): void;
function useEventListener<K extends keyof ElementEventMap>(
eventName: K,
handler: (e: ElementEventMap[K]) => void,
options: IOptions<Element>
): void;
function useEventListener<K extends keyof DocumentEventMap>(
eventName: K,
handler: (e: DocumentEventMap[K]) => void,
options: IOptions<Document>
): void
function useEventListener(eventName: string, handler: Function, options: IOptions) {
const handlerRef = useRef(handler)
handlerRef.current = handler
useEffect(() => {
const targetElement = getTargetElement(options.target, window)!
if (!targetElement.addEventListener) {
return
}
//AddEventListenerOptions 增加了once和passive两个选项
const eventListener = (e: Event): EventListenerOrEventListenerObject | AddEventListenerOptions => {
return handlerRef.current && handlerRef.current(e)
}
targetElement.addEventListener(eventName, eventListener, {
once: options.once,
passive: options.passive,
capture: options.capture
})
return () => {
targetElement.removeEventListener(eventName, eventListener, {
capture: options.capture
})
}
}, [])
}
 

16. useEventTarget

常见表单控件(通过 e.target.value 获取表单值) 的 onChange 跟 value 逻辑封装,支持自定义值转换和重置功能。

使用示例:

import React, { Fragment } from 'react';
import { useEventTarget } from 'ahooks';
export default () => {
const [value, { reset, onChange }] = useEventTarget({ initialValue: 'this is initial value' });
return (
<Fragment>
<input value={value} onChange={onChange} style={{ width: 200, marginRight: 20 }} />
<button type="button" onClick={reset}>
reset
</button>
</Fragment>
);
};

实现: 

  1. 确定输入输出:

最主要的功能其实就是拿到表单中的值,所以输入可以是 initialValue:"默认值" ,而输出,必须是表单的value,以及接收表单值变化的onChange函数

type EventTarget<T>={
target: {
value:T
}
}
export default function useTargetEvent<T>(initialValue: T): [T, (e: EventTarget<T>) => any];
  1. 实现基础功能
export default function useTargetEvent<T>(initialValue: T): [T, (e: EventTarget<T>) => any] {
const [value, setValue] = useState(initialValue)
const onChange = useCallback((e: EventTarget<T>) => {
setValue(e.target.value)
}, [])
return [
value,
onChange
]
}
  1. 实现reset功能
export default function useTargetEvent<T>(initialValue: T) {
const [value, setValue] = useState(initialValue)
// 只需要重置到初始值,所以useCallback依赖项为空数组
const reset = useCallback(() => setValue(initialValue), [])
const onChange = useCallback((e: EventTarget<T>) => {
setValue(e.target.value)
}, [])
return [
value,
{
onChange,
reset
}
]
}
  1. 实现自定义值转换功能
type EventTarget<U> = {
target: {
value: U
}
}
// 使用了泛型T 和 U,是因为在经过transformer转换前,
// target.value的类型不一定与initivalValue类型相同
interface IOptions<T, U> {
transformer: (e: U) => T,
initialValue: T
}
export default function useTargetEvent<T, U = T>(e: IOptions<T, U>) {
const { initialValue, transformer } = e
const [value, setValue] = useState(initialValue)
const transformerRef = useRef(transformer)
transformerRef.current = transformer
// 只需要重置到初始值,所以useCallback依赖项为空数组
const reset = useCallback(() => setValue(initialValue), [])
const onChange = useCallback((e: EventTarget<U>) => {
const _value = e.target.value
// 判断确transformer是否存在
if (typeof transformerRef.current === "function") {
const value = transformerRef.current(_value)
return setValue(value)
}
return setValue(_value as any as T)
}, [])
return [
value,
{
onChange,
reset
}
] as const // as const TS会将其解析为常量,没有as const ,TS会解析你返回的是(T|{...})[]
}

17.usePersistFn

在某些场景中,你可能会需要用 useCallback 记住一个回调,但由于内部函数必须经常重新创建,记忆效果不是很好,导致子组件重复 render。对于超级复杂的子组件,重新渲染会对性能造成影响。通过 usePersistFn,可以保证函数地址永远不会变化。

  1. 使用
type noop = (...args: any[]) => any;
const fn = usePersistFn<T extends noop>(
fn: T,
);
  1. 分析
    1. 传入usePersistFn(fn)中的fn地址可以不断变化,但是返回的函数地址不变
    2. 只有使用useRef可以拿到最新的函数,而不会导致函数组件更新,

实现:

type noop = (...args: any[]) => any
export default function usePersistFn<T extends noop>(fn: T): T {
// 使用useRef记住外部传入的函数fn
const fnRef = useRef(fn)
fnRef.current = fn
// 使用useRef返回一个地址不会变化的函数
const persistFnRef = useRef<T>()
if (!persistFnRef.current) {
persistFnRef.current = function (this: any, ...args: any[]) {
return fnRef.current.apply(this, args)
} as T
}
return persistFnRef.current
}

18. useScroll

获取元素的滚动状态。

  1. 使用
const position = useScroll(target, shouldUpdate);
// position = {top:number,left:number}
// target= HTMLElement | () => HTMLElement | Document |MutableRefObject
// shouldUpdate = ({ top: number, left: number}) => boolean

实现 :

  1. 确定输入输出:
// 调用函数useScroll后就是返回position
interface Position {
left: number
top: number
}
export type Target = BasicTarget<HTMLElement | Document>// 监听的目标类型
export type ScrollListenController = (val: Position) => boolean // onScroll的控制器,返回布尔值控制是否更新position
function useScroll(target?: Target, shouldUpdate: ScrollListenController = () => true): Position
 
  1. 给目标dom绑定scroll事件
export default function useScroll(target?: Target, shouldUpdate: ScrollListenController = () => true) {
useEffect(() => {
const el = getTargetElement(target, document)!
if (!el.addEventListener) {
return
}
function listener(event: Event) {
}
el.addEventListener("scroll", listener)
return () => {
return el.removeEventListener("scroll", listener)
}
})// 依赖项为空,每次组件刷新都需要重新给dom绑定scroll事件
}
  1. 更新position
export default function useScroll(target?: Target, shouldUpdate: ScrollListenController = () => true) {
const [position, setPosition] = useState<Position>({
left: NaN,
top: NaN
})
// 持久化shouldUpdate
const shouldUpdatePersistFn = usePersistFn(shouldUpdate)
useEffect(() => {
const el = getTargetElement(target, document)!
if (!el.addEventListener) {
return
}
function updatePosition(currentTarget: Target) {
let newPosition: Position;
if (currentTarget === document) {
if (!currentTarget.scrollingElement) {
return
}
// 桌面端和移动端的窗体滚动元素是不一样的
// document.documentElement.scrollTop; 桌面端
// document.body.scrollTop; 移动端
// 为了兼容移动端和桌面端,可以使用document.scrollingElement,可以自动识别不同平台上的滚动容器。
// https://www.zhangxinxu.com/wordpress/2019/02/document-scrollingelement/
newPosition = {
left: currentTarget.scrollingElement.scrollLeft,
top: currentTarget.scrollingElement.scrollTop
}
} else {
newPosition = {
left: (currentTarget as HTMLElement).scrollLeft,
top: (currentTarget as HTMLElement).scrollTop
}
}
// 返回true才更新position
if (shouldUpdatePersistFn(position)) {
setPosition(newPosition)
}
}
// 初始化时,更新position
updatePosition(el as Target)
function listener(event: Event) {
if (!event.target) {
return;
}
updatePosition(event.target as Target)
}
el.addEventListener("scroll", listener)
return () => {
return el.removeEventListener("scroll", listener)
}
})
return position;
}

回复

我来回复
  • 暂无回复内容