Loading...
墨滴

胡志武

2021/04/07  阅读:35  主题:默认主题

18个好用的自定义react hook

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 (thisany, ...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 (thisany, ...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(thisany, ...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 : statehandleSetStateas const
 }

11. useMap

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

import { useState, useMemo } from "react";

// 只要有Iterable接口就可以做map的参数
export default function useMap<KT>(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>// domref

再完善些,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: 200marginRight: 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<TU = 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 (thisany, ...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;
}

胡志武

2021/04/07  阅读:35  主题:默认主题

作者介绍

胡志武