18个好用的自定义react hook

胡志武 2021-04-07 20:32:49
好用 react 自定义 自定 定义


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;
}
版权声明
本文为[胡志武]所创,转载请带上原文链接,感谢
https://mdnice.com/writing/23f00c273e6445fcb43b6352a1109e8a

  1. VSLAM front end: image feature extraction
  2. Exclusive dialogue with the person in charge of Alibaba cloud function computing: what you don't know about serverless
  3. 「开源免费」基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之序列号自定义组件(四)
  4. "Open source and free" serial number customization component of crudapi background management system of front end spa project based on Vue and Quasar (4)
  5. JavaScript 相似度排序
  6. Springboot项目搭建(前端到数据库,超详细)
  7. Less than 150 lines of code to write a python version of the snake
  8. 02_Nginx部署服务
  9. vue 快速入门 系列 —— vue 的基础应用(上)
  10. JavaScript similarity ranking
  11. 基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之布局菜单嵌套路由(三)
  12. Springboot project construction (front end to database, super detailed)
  13. 02_ Nginx Deployment Services
  14. vue 快速入门 系列 —— vue 的基础应用(上)
  15. Vue quick start series basic application of Vue
  16. Layout menu nested routing of front end spa project crudapi background management system based on Vue and Quasar (3)
  17. Vue quick start series basic application of Vue
  18. 一个好用的Visual Studio Code扩展 - Live Server,适用于前端小工具开发
  19. 基于Vue和Quasar的前端SPA项目实战之用户登录(二)
  20. css常用选择器总结
  21. Behind the miracle of the sixth championship is the football with AI blessing in the Bundesliga
  22. An easy to use Visual Studio code extension - live server, suitable for front-end gadget development
  23. 用 Python 抓取公号文章保存成 HTML
  24. User login of front end spa project based on Vue and Quasar (2)
  25. Summary of common selectors in CSS
  26. Using Python to grab articles with public number and save them as HTML
  27. To "restless" you
  28. 【免费开源】基于Vue和Quasar的crudapi前端SPA项目实战—环境搭建 (一)
  29. 【微信小程序】引入阿里巴巴图标库iconfont
  30. layui表格点击排序按钮后,表格绑定事件失效解决方法
  31. Unity解析和显示/播放GIF图片,支持http url,支持本地file://,支持暂停、继续播放
  32. 【vue】 export、export default、import的用法和区别
  33. [free and open source] crudapi front end spa project based on Vue and Quasar
  34. [wechat applet] introduces Alibaba icon library iconfont
  35. Layui table click Sort button, table binding event failure solution
  36. Element树形控件Tree踩坑:修改current-node-key无效
  37. Unity parses and displays / plays GIF images, supports HTTP URL, supports local file: / /, supports pause and resume playback
  38. Element树形控件Tree踩坑:修改current-node-key无效
  39. The usage and difference of export, export default and import
  40. Element tree control: invalid to modify current node key
  41. Element tree control: invalid to modify current node key
  42. linux下安装apache(httpd-2.4.3版本)各种坑
  43. How to install Apache (httpd-2.4.3) under Linux
  44. 程序员业余时间写的代码也算公司的?Nginx之父被捕引发争议
  45. Nacos serialize for class [com.alibaba.nacos.common.http.HttpRestResult] failed.
  46. Do programmers write code in their spare time? Controversy over the arrest of nginx's father
  47. Nacos serialize for class [ com.alibaba.nacos . common.http.HttpRestResult ] failed.
  48. Seamless management of API documents using eolink and gitlab
  49. vue 的基础应用(上)
  50. 28岁开始零基础学前端,这些血的教训你一定要避免
  51. Basic application of Vue
  52. Starting at the age of 28, you must avoid these bloody lessons
  53. Ubuntu 16.04 can not connect to the wireless solution and QQ installation
  54. Industry security experts talk about the rapid development of digital economy, how to guarantee the security of data elements?
  55. 利用Vue实现一个简单的购物车功能
  56. Behind the "tireless classroom" and teacher training, can byte education really "work wonders"?
  57. Using Vue to realize a simple shopping cart function
  58. 【css】伪类和伪类元素的区别
  59. 【css效果】实现简单的下拉菜单
  60. 【vue】父子组件传值