likes
comments
collection
share

前端人 精学ahooks源码

作者站长头像
站长
· 阅读数 4

背景

特别喜欢红宝书的一句话“站在巨人的肩上”,起因是觉得react中的hooks语法,用了那么长时间,有的时候还是似懂非懂,所以才有了这一篇文章,学习一下前人封装hook的方法。沉浸式学习,从浅入深,一点一点吃透,也希望大家能够从 理解思想 -> 输出思想,这样社区才能越来越好,帮助自己也帮助大家。如果文章有错误,也希望各位指点一二。 本文将持续迭代,赶紧收藏起来吧,收藏===学会

前置知识

前言

  • 如果react hooks语法不熟练,请确保看过一遍官方文档,为了让代码简洁,react hooks将不再引入,当它存在即可
  • 文章会直接跳过一些边界情况,isFuncisBroswer等,如有遗漏这些工具函数判断,自行判断下,以及大部分Ts类型,单元测试等。把更多时间专注于hooks的封装,避免造成一些心智负担
  • 一些辅助函数useLatestuseMemoizedFnuseUpdateEffect等,就不再引入,默认当它存在即可
  • 保留了大部分ahooks源码,但是我更改了一部分,我觉得更改了一部分源码,才知道你学习的ahooks源码,这是我有意为之
  • 最佳学习路线,照着ahooks官网,一遍看案例,一边对照源码学习,有疑惑的地方先思考,再动手,动手,一定要动手

状态就像快照

  const [count, setCount] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1) // count: 0
    }, 1000)

    return () => clearInterval(timer)
  }, [])
  • 执行过程:
    • 页面挂载后,开启定时器,1s以后,执行回调函数,此时的count为0,递增它
    • 2s以后,再次回调计时器函数,此时的count还是0,状态就好比快照(闭包问题,effect中的回调函数只会执行一次,拿到的初始化的值)
    • 无论过了多久,回调中的count永远是0
    • 当组件卸载后,清除定时器
  • 传送门:

获取最新的值

  • useLatest 返回当前最新的值
function useLatest<T>(value: T) {
  const ref = useRef(value)
  ref.current = value

  return ref
}
  • 解决闭包问题
const [count, setCount] = useState(0)
const latestCount = useLatest(count)

useEffect(() => {
  const timer = setInterval(() => {
    setCount(latestCount.current + 1)
  }, 1000)

  return () => clearInterval(timer)
}, [])
  • 代码解析
    • latestCount每次可以拿到最新的值,其本质就是利用useRef
    • 组件初始化时,useRef的初始值为count(即0),后续渲染获取最新的count进行赋值

辅助函数

useMemoizedFn

  • 用于代替useCallback,当它的deps依赖发生变更,它返回的函数地址会变化,而useMemoizedFn不会
const useMemoizedFn = (fn) => {
  const fnRef = useRef(fn)
  fnRef.current = useMemo(() => fn, [fn])

  const memoizedFn = useRef()
  if (!memoizedFn.current) {
    memoizedFn.current = function (...args) {
      // return fn(...args)
      return fnRef.current(...args)
    }
  }

  return memoizedFn.current
}
  • 执行过程:
    • 当使用该hook的组件,组件重新执行,意味着fn回调重新定义
    • 保存该fn函数,每次拿到最新的fn,避免闭包问题,不使用fnRef保存最新的函数引用,那么意味着,fn中拿到的state不会是最新的,即(return fn(...args))
    • 本质上是通过memoizedFn去执行fnRef函数,返回的是一个ref,所以保证函数地址永远不会变化。

useUpdateEffect / useUpdateLayoutEffect

  • 忽略首次执行,只有依赖发生改变才会执行
const useUpdateEffect = createUpdateEffect(useEffect)
const useUpdateLayoutEffect = createUpdateEffect(useLayoutEffect)

function createUpdateEffect(hook) {
  return (effect, deps) => {
    const isMounted = useRef(false)

    // 热更新 重置
    hook(
      () => () => {
        isMounted.current = false
      },
      []
    )
    hook(() => {
      if (!isMounted.current) {
        isMounted.current = true
      } else {
        return effect()
      }
    }, deps)
  }
}
  • 就定义一个ref状态即可,isMounted控制是否已经挂载

LifeCycle

useMount

  • 组件初始化时执行
const useMount = (fn: () => void) => {
  useEffect(() => {
    fn?.()
  }, [])
}

useUnmount

  • 组件卸载时执行
const useUnmount = (fn: () => void) => {
  const fnRef = useLatest(fn)

  useEffect(
    () => () => {
      fnRef.current()
    },
    []
  )
}

useUnmountedRef

  • 当前组件是否已经卸载
const useUnmountedRef = () => {
  const unmountedRef = useRef(false)
  useEffect(() => {
    unmountedRef.current = false
    return () => {
      unmountedRef.current = true
    }
  }, [])
  return unmountedRef
}

State

useSetState

  • 案例说明
  const [state, setState] = useSetState({
    name: 'ice',
    age: 24,
  })
  
  <div>
    <p>{JSON.stringify(state)}</p>
    <button onClick={() => setState({ age: ++state.age })}>age:+1</button>
  </div>
  )
  • 源码剖析
    • 可用于合并对象,基本与class中的this.setState一致
    • useCallback用于性能优化,如果组件更新重新执行函数,则setMergeState重新定义(引用不同,如果传递给子组件,导致子组件进行不必要的执行)
const isFunc = (val) => typeof val === 'function'

export const useSetState = (initialState) => {
  const [state, setState] = useState(initialState)

  const setMergeState = useCallback((patch) => {
    setState((prevState) => {
      const newState = isFunc(patch) ? patch(prevState) : patch
      return newState ? { ...prevState, ...newState } : prevState
    })
  }, [])

  return [state, setMergeState]
} 

useToggle

const useToggle = (defaultVal = false, reverseValue) => {
  const [state, setState] = useState(defaultVal)

  const actions = useMemo(() => {
    const reverseValueOrigin = reverseValue === undefined ? !defaultVal : reverseValue
    const toggle = () => setState((s) => (s === reverseValueOrigin ? defaultVal : reverseValueOrigin))
    const set = (v) => setState(v)
    const setLeft = () => setState(defaultVal)
    const setRight = () => setState(reverseValueOrigin)

    return { toggle, set, setLeft, setRight }
  }, [])

  return [state, actions]
}
  • useMemo用于性能优化,使用该hook的组件重新渲染,actions能够缓存计算结果
  • setState回调函数的写法,则能拿到上一次的值
  • 传送门:react.docschina.org/reference/r…

useBoolean

const useBoolean = (defaultVal: boolean = false) => {
  const [state, { toggle, set }] = useToggle(!!defaultVal)

  const actions = useMemo(() => {
    const setTrue = () => set(true)
    const setFalse = () => set(false)
    return {
      toggle,
      set: (v) => set(!!v),
      setTrue,
      setFalse,
    }
  }, [])

  return [state, actions]
}
  • 本质就是调用useToggle

useLocalStorageState / useSessionStorageState

export type SetState<S> = S | ((prevState?: S) => S)
export interface Options<T> {
  defaultValue?: T | (() => T)
  serializer?: (value: T) => string
  deserializer?: (value: string) => T
  onError?: (error: unknown) => void
}
const isBroswer = true
const isFunc = (v: unknown): v is (...args: any) => any => typeof v === 'function'

export const useLocalStorageState = createLocalStorageState(() => (isBroswer ? localStorage : undefined))
export const useSessionStorageState = createLocalStorageState(() => (isBroswer ? sessionStorage : undefined))

function createLocalStorageState(getStorage: () => Storage | undefined) {
  function useStorageState<T>(key: string, options: Options<T> = {}) {
    let storage: Storage | undefined
    const {
      onError = (e) => {
        console.error(e)
      },
    } = options

    try {
      storage = getStorage()
    } catch (e) {
      onError(e)
    }
    // 序列化
    const serializer = (value: T) => {
      if (options.serializer) {
        return options.serializer(value)
      }
      return JSON.stringify(value)
    }
    // 反序列化
    const deserializer = (value: string) => {
      if (options.deserializer) {
        return options.deserializer(value)
      }
      return JSON.parse(value)
    }
    
    function getStorageValue() {
      const raw = storage?.getItem(key)
      if (raw) {
        return deserializer(raw)
      }
      if (isFunc(options.defaultValue)) {
        return options.defaultValue()
      }
      return options.defaultValue
    }

    function updateState(value?: SetState<T>) {
      const currentState = isFunc(value) ? value(state) : value
      setState(currentState)

      if (currentState === undefined) {
        storage?.removeItem(key)
      } else {
        storage?.setItem(key, serializer(currentState))
      }
    }
    // 当key修改后,重新获取值
    useUpdateEffect(() => {
      setState(getStorageValue())
    }, [key])

    const [state, setState] = useState(getStorageValue)

    return [state, useMemoizedFn(updateState)]
  }

  return useStorageState
}
  • 核心函数
    • getStorageValue
      • 先获取,有的话用本地值,没有用默认值
    • updateState
      • 更新最新的值,如果为undefined移除当前缓存值
    • serializer
      • 序列化,有传入的优先使用传入的,没有直接序列化
    • deserializer
      • 反序列化,有传入的优先使用传入的,没有直接反序列化

useDebounce / useDebounceFn

import { debounce } from 'lodash-es'

function useDebounce(value, options) {
  const [debounced, setDebounced] = useState(value)

  const { run } = useDebounceFn(() => {
    setDebounced(value)
  }, options)

  useEffect(() => {
    run()
  }, [value])

  return debounced
}

function useDebounceFn(fn, options) {
  const fnRef = useLatest(fn)
  const wait = options?.wait ?? 1000

  const debounced = useMemo(
    () =>
      debounce(
        (...args) => {
          fnRef.current(...args)
        },
        wait,
        options
      ),
    []
  )
  const { cancel, flush } = debounced

  useUnmount(() => {
    cancel()
  })

  return { run: debounced, cancel, flush }
}
  • useDebounce
    • debounced是被防抖以后的值,只要在合适的时机调用setDebounced即可,页面进行挂载或原始值value更新,就执行run函数,而run函数,是useDebounceFn的返回值
  • useDebounceFn
    • 第一个参数fn(即回调函数),拿到最新的fn函数(因为被useMemo包裹)
    • debounced本质上就是debounce函数的返回值,而debounce函数,来自lodash
    • 当页面卸载的时候调用cancel,避免内存泄漏

useThrottle / useThrottleFn

  • 实现思路和 useDebounce / useDebounceFn 一致

useMap

const useMap = (initialValue) => {
  const getInitialValue = () => new Map(initialValue)
  const [map, setMap] = useState(getInitialValue)

  const set = (key, val) => {
    setMap((prev) => {
      const temp = new Map(prev)
      temp.set(key, val)
      return temp
    })
  }

  const setAll = (newMap) => {
    setMap(new Map(newMap))
  }

  const remove = (key) => {
    setMap((prev) => {
      const temp = new Map(prev)
      temp.delete(key)
      return temp
    })
  }

  const reset = () => setMap(getInitialValue())

  const get = (key) => map.get(key)

  return [
    map,
    {
      set: useMemoizedFn(set),
      setAll: useMemoizedFn(setAll),
      remove: useMemoizedFn(remove),
      reset: useMemoizedFn(reset),
      get: useMemoizedFn(get),
    },
  ]
}
  • 引用类型,需要视图更新,需要浅拷贝(即赋值一个新的map即可),因为setState本质上会进行浅比较,(即前一个值和当前值比较Object.is(prev, cur))

useSet

function useSet(initialValue) {
  const getInitialValue = () => new Set(initialValue)
  const [set, setSet] = useState(getInitialValue)

  const add = (key) => {
    if (set.has(key)) {
      return
    }
    setSet((prev) => {
      const temp = new Set(prev)
      temp.add(key)
      return temp
    })
  }

  const remove = (key) => {
    if (!set.has(key)) {
      return
    }
    setSet((prev) => {
      const temp = new Set(prev)
      temp.delete(key)
      return temp
    })
  }

  const reset = () => setSet(getInitialValue())

  return [
    set,
    {
      add: useMemoizedFn(add),
      remove: useMemoizedFn(remove),
      reset: useMemoizedFn(reset),
    },
  ]
}

  • useMap的思想基本一致,add添加元素,如果存在直接return,减少没必要的重新渲染,remove同理,如果不存在,减少没必要的渲染

usePrevious

const defaultShouldUpdate = (a, b) => !Object.is(a, b)

function usePrevious(state, shouldUpdate = defaultShouldUpdate) {
  const curRef = useRef(state)
  const prevRef = useRef()

  if (shouldUpdate(curRef.current, state)) {
    prevRef.current = curRef.current
    curRef.current = state
  }

  return prevRef.current
}
  • 双指针,利用两个ref,保存前一个值和当前值即可

useSafeState

function useSafeState(initialState) {
  const unmountedRef = useUnmountedRef()
  const [state, setState] = useState(initialState)

  const setSafeState = useCallback((nextState) => {
    if (unmountedRef.current) return
    setState(nextState)
  }, [])

  return [state, setSafeState]
}
  • 防止组件卸载后,异步回调引起的内存泄漏,判断当前组件是否已经卸载即可

useGetState

function useGetState(initialState) {
  const [state, setState] = useState(initialState)
  const latestRef = useLatest(state)

  const getState = useCallback(() => latestRef.current, [])

  return [state, setState, getState]
}

useResetState

function useResetState(initialState) {
  const [state, setState] = useState(initialState)

  const resetState = useCallback(() => {
    setState(initialState)
  }, [])

  return [state, setState, resetState]
}
  • 利用useCallback, 每次当hook重新执行,而resetState中的回调函数拿到的还是初始化的值,利用闭包

Advanced

useCreation

class Foo {
  constructor() {
    this.data = Math.random()
  }
  data: number
}
// 1. 当所在的函数式组件,重新执行的时候,Foo类也重新执行
const foo1 = useRef(new Foo())
// 2. 而它Foo只会执行一次
const foo2 = useCreation(() => new Foo(), [])))
  • 代替useRef或者useMemo, 因为useMemo不一定会被重计算,而useRef在创建一些复杂对象的时候,可能会存在潜在的性能问题
const depsAreSame = (oldDeps: DependencyList, deps: DependencyList) => {
  if (oldDeps === deps) return true
  for (let i = 0; i < oldDeps.length; i++) {
    if (!Object.is(oldDeps[i], deps[i])) return false
  }
  return true
}

export function useCreation<T>(factory: () => T, deps: DependencyList) {
  const { current } = useRef({
    deps,
    obj: undefined as T | undefined,
    initialized: false,
  })

  if (!current.initialized || !depsAreSame(current.deps, deps)) {
    current.deps = deps
    current.obj = factory()
    current.initialized = true
  }

  return current.obj as T
}
  • 只有两种情况下会重新执行factory
    1. 初始化的时候
    2. 当依赖更新,依赖不相等的时候

useReactive

前置知识

  • WeakMap
    let obj = { name: 'ice', age: '20' }
    const m = new WeakMap() // Map则回阻止obj的回收
    m.set(obj, 'xxx')
    
    const clearup = new FinalizationRegistry((key) => {
      console.log(key, '被垃圾回收了')
    })
    clearup.register(obj, 'obj')
    obj = null
    
    • useReactive中使用到了WeakMap,若某个obj被它引用为key,当这个obj被赋值为null时,它不会阻止垃圾回收该对象(即弱引用),则Map强引用
  • Proxy
    • vue3中采用Proxy,监听代理对象的变化,从而引起视图的变化
    const obj = {
     name: 'ice',
     age: 24,
     get fullname() {
       console.log(this)
       return this.name + this.age
     },
    }
    
    const proxy = new Proxy(obj, {
     get(target, key, receiver) {
       console.log('get 触发')
       console.log(target === obj) // true
       console.log(key)
       console.log(receiver === proxy) // true
    
       // return target[key]
       return Reflect.get(target, key, receiver)
     },
    })
    
    • 其中Reflect的作用进行反射,改变访问对象触发getter时的this指向,不然this指向的则是obj对象

源码实现

  • 不过度考虑边缘情况
// 回调该函数,引起视图的变化
function useUpdate() {
  const [, setState] = useState({})

  return useCallback(() => {
    setState({})
  }, [])
}

const proxyMap = new WeakMap() // [k:v] [proxy:rawObj]

// 返回proxy对象,监听该对象的变化,执行回调函数
function observer(initialVal, cb: () => void) {
  const existsingProxy = proxyMap.get(initialVal)
  // 如果存在proxy,直接使用存在的
  if (existsingProxy) {
    return existsingProxy
  }

  const proxy = new Proxy(initialVal, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      return typeof res === 'object' ? observer(res, cb) : res
    },
    set(target, key, newValue, receiver) {
      const res = Reflect.set(target, key, newValue, receiver)
      cb()
      return res
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key)
      cb()
      return res
    },
  })
  
  proxyMap.set(initialVal, proxy)

  return proxy
}

function useReactive(initialVal) {
  const update = useUpdate()
  const stateRef = useRef(initialVal)

  return useMemo(() => observer(stateRef.current, update), [])
}
  • 核心实现
    • 返回一个代理对象,即(proxy)对象
    • 定义proxy捕获器,get set deleteProperty
    • 调用update, 触发页面更新

Effect

useAsyncEffect

// error
useEffect(async () => {
  const res = await getName()
  setName(res)
}, [])

// success
useAsyncEffect(async () => {
  const res = await getName()
  setName(res)
}, [])
  • 支持异步函数,因为useEffect中的effect不能传入异步函数
const isAsyncGenerator = (val) => val[Symbol.asyncIterator]
function useAsyncEffect(effect, deps) {
  useEffect(() => {
    const e = effect()

    let cancelled = false
    async function execute() {
      if (isAsyncGenerator(e)) {
        while (true) {
          const res = await e.next()
          if (res.done || cancelled) {
            break
          }
        }
      } else {
        await e
      }
    }
    execute()
    return () => {
      cancelled = true
    }
  }, deps)
}
  • 分为以下两种情况
    1. 异步函数直接调用
    2. 异步生成器函数,不断的调用next,直至done, 或者当依赖更新,执行effect[clearup]函数,修改cancelled循环结束

useDebounceEffect

function useDebounceEffect(effect: EffectCallback, deps?: DependencyList, options?: DebounceOptions) {
  const [flag, setFlag] = useState({})

  const { run } = useDebounceFn(() => {
    setFlag({})
  }, options)

  useEffect(() => {
    run()
  }, deps)

  useUpdateEffect(effect, [flag])
}
  • 本质上对effect做了转发,当flag改变时,引起视图的更新
  • deps的依赖更新,触发useEffect的回调函数执行,而此时的run函数是一个被防抖过的函数

useThrottleEffect

function useThrottleEffect(effect: EffectCallback, deps?: DependencyList, options?: ThrottleOptions) {
  const [flag, setFlag] = useState({})

  const { run } = useThrottleFn(() => {
    setFlag({})
  }, options)

  useEffect(() => {
    run()
  }, deps)

  useUpdateEffect(effect, [flag])
}
  • 逻辑跟useDebounceEffect一致

useDeepCompareEffect / useDeepCompareLayoutEffect

const useDeepCompareEffect = createDeepCompareEffect(useEffect)
const useDeepCompareLayoutEffect = createDeepCompareEffect(useLayoutEffect)

const createDeepCompareEffect = (hook) => (effect, deps) => {
  const signalRef = useRef<number>(0)
  const previous = usePrevious(deps)

  if (deps === undefined || !isEqual(deps, previous)) {
    signalRef.current++
  }

  hook(effect, [signalRef.current])
}
  • 增加一个signalRef控制在合适的时机调用effect即可,其中isEqual是调用lodash的,判断两者是否深度相等

useInterval / useTimeout

const isNumber = (val: unknown): val is number => typeof val === 'number'
const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {
  const timerCallback = useMemoizedFn(fn)
  const timerRef = useRef<number | null>(null)

  const clear = useCallback(() => {
    if (timerRef.current) {
      clearInterval(timerRef.current) // clearTimeout
    }
  }, [])

  useEffect(() => {
    if (!isNumber(delay) || delay < 0) {
      return
    }
    if (options.immediate) {
      timerCallback()
    }
    timerRef.current = setInterval(timerCallback, delay) // setTimeout
    return clear
  }, [delay, options.immediate])

  return clear
}
  • 执行顺序
    1. 页面挂载前,fn函数被useMemoizedFn包裹(当FC重新执行,该timerCallback引用从始至终都是同一个)
    2. 进行一些变量定义
    3. 把清除定时器的clear函数返回
    4. 页面挂载后,执行useEffect,进行一些边界处理并且开启定时器,当delay/immediate改变后,重新执行FC

useUpdate

  • useReactive中实现过

useLockFn

function useLockFn<P extends any[] = any[], V = any>(fn: (...args: P) => Promise<V>) {
  const lockRef = useRef(false)

  return useCallback(
    async (...args: P) => {
      if (lockRef.current) return
      lockRef.current = true
      try {
        const ret = await fn(...args)
        return ret
      } finally {
        lockRef.current = false
      }
    },
    [fn]
  )
}
  • 使用场景
    • 可以用来代替部分防抖场景
  • 其本质就是通过一个ref变量,控制fn是否执行即可

Dom

dom类的设计到许多的工具函数,忽略一些边缘情况,下面一起来看下吧~

工具函数

getTargetElement

function getTargetElement(target) {
  let targetElement

  if (isFunc(target)) {
    targetElement = target()
  } else if ('current' in target) {
    targetElement = target.current
  } else {
    targetElement = target
  }

  return targetElement
}

depsAreSame

const depsAreSame = (oldDeps: DependencyList, deps: DependencyList) => {
  if (oldDeps === deps) return true

  for (let i = 0; i < oldDeps.length; i++) {
    if (!Object.is(oldDeps[i], deps[i])) return false
  }

  return true
}
  • 判断依赖是否相同,分为两种情况
    • 同一个引用
    • 遍历新老deps,Object.is判断是否相同

useEventListener

useEventListener的执行顺序稍微有点绕,我们来看下流程

useEventListener
useEffectWithTarget
createEffectWithTarget
returnValue
  1. useEventListener内部本质调用useEffectWithTarget
  2. useEffectWithTarget则是createEffectWithTarget(利用闭包封装useEffect/useLayoutEffect
  3. 最后的入参都会被useEffectWithTarget接收到,也就是createEffectWithTarget的返回值
// useEffectWithTarget.ts
const useEffectWithTarget = createEffectWithTarget(useEffect)

// createEffectWithTarget.ts
export const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
  const useEffectWithTarget = (effect: EffectCallback, deps: DependencyList, target) => {
    const hasInitRef = useRef<boolean>(false)
    const lastElementRef = useRef<Element[]>([])
    const lastDepsRef = useRef<DependencyList>([])

    const unloadRef = useRef<any>()

    useEffectType(() => {
      const targets = isArray(target) ? target : [target]
      const els = targets.map((el) => getTargetElement(el))

      if (!hasInitRef.current) {
        hasInitRef.current = true
        lastElementRef.current = els
        lastDepsRef.current = deps

        unloadRef.current = effect()
      }

      if (els.length !== lastElementRef.current.length || !depsAreSame(lastDepsRef.current, deps) || !depsAreSame(lastElementRef.current, els)) {
        unloadRef.current?.()

        lastElementRef.current = els
        lastDepsRef.current = deps
        unloadRef.current = effect()
      }
    })

    useUnmount(() => {
      unloadRef.current?.()
      hasInitRef.current = false
    })
  }

  return useEffectWithTarget
}

function useEventListener(eventName: string, handler: noop, options: Options = {}) {
  const handlerRef = useLatest(handler)

  useEffectWithTarget(
    () => {
      const el = getTargetElement(options.target)
      if (!el?.addEventListener) return

      const eventListener = (e) => {
        handlerRef.current(e)
      }

      el.addEventListener(eventName, eventListener, {
        capture: options.capture,
        once: options.once,
        passive: options.passive,
      })

      return () => {
        el.removeEventListener(eventName, eventListener, {
          capture: options.capture,
        })
      }
    },
    [eventName, options.once, options.passive],
    options.target
  )
}

createEffectWithTarget

  • 其本质,就是控制effect在合适的时机执行
    1. 初始化执行一次
    2. els长度不一致、els发生改变或deps发生改变执行

useEventListener -> useEffectWithTarget

  • 只是监听/移除事件

useDocumentVisibility

type VisibilityState = 'hidden' | 'visible' | 'prerender' | undefined

function getVisibility() {
  if (!document) return 'visible'
  return document.visibilityState
}

function useDocumentVisibility(): VisibilityState {
  const [documentVisibility, setDocumentVisibility] = useState<VisibilityState>(getVisibility)

  useEventListener(
    'visibilitychange',
    () => {
      console.log(getVisibility())
      setDocumentVisibility(getVisibility())
    },
    {
      target: () => document,
    }
  )

  return documentVisibility
}
  • 实现比较简单,监听一下visibilitychange即可

useTitle

interface Options {
  restoreOnUnmount?: boolean
}
const DEFAULT_OPTIONS: Options = {
  restoreOnUnmount: false,
}

function useTitle(title: string, options: Options = DEFAULT_OPTIONS) {
  const titleRef = useRef(document.title)

  useEffect(() => {
    document.title = title
  }, [title])

  useUnmount(() => {
    if (options.restoreOnUnmount) {
      document.title = titleRef.current
    }
  })
}
  • 实现比较简单
    • 第一次进入先把原有的title保存下来
    • 挂载/当title改变后,修改title
    • 页面卸载后,根据restore判断是否恢复之前的title

useFavicon

import { useEffect } from 'react'

const ImgTypeMap = {
  SVG: 'image/svg+xml',
  ICO: 'image/x-icon',
  GIF: 'image/gif',
  PNG: 'image/png',
}

type ImgTypeKeys = keyof typeof ImgTypeMap

export function useFavicon(href: string) {
  useEffect(() => {
    if (!href) return

    const curUrl = href.split('.')
    const suffix = curUrl[curUrl.length - 1].toUpperCase() as ImgTypeKeys
    // 1. 创建
    const link: HTMLLinkElement = document.querySelector("link[rel*='icon']") || document.createElement('link')
    link.type = ImgTypeMap[suffix]
    link.rel = 'shortcut icon'
    link.href = href
    // 2. 挂载
    document.querySelector('head')?.append(link)
  }, [href])
}

useHover

interface Options {
  onEnter: () => void
  onLeave: () => void
  onChange: (isHovering: boolean) => void
}

function useHover(target, options: Record<string, any> = {}): boolean {
  const { onEnter, onLeave, onChange } = options
  const [state, { setTrue, setFalse }] = useBoolean()

  useEventListener(
    'mouseleave',
    () => {
      onLeave?.()
      onChange?.(false)
      setFalse()
    },
    {
      target,
    }
  )
  useEventListener(
    'mouseenter',
    () => {
      onEnter?.()
      onChange?.(true)
      setTrue()
    },
    {
      target,
    }
  )

  return state
}

实现较为简单,本质还是使用的useEventListener,回调对应的函数即可

useMutationObserver

前置知识 MutationObserver

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MutationObserver</title>
  </head>
  <style>
    .box {
      width: 120px;
      height: 120px;
      background: pink;
    }
  </style>
  <body>
    <div class="box"></div>
    <button>+10</button>
    <script>
      const boxEl = document.querySelector('.box')
      const btnEl = document.querySelector('button')

      const cb = ([item], observer) => {
        console.log(item.target.style.width)
      }
      const observer = new MutationObserver(cb)
      observer.observe(boxEl, {
        attributes: true,
      })

      btnEl.addEventListener('click', () => {
        const width = boxEl.clientWidth
        boxEl.style.width = `${boxEl.clientWidth + 10}px`
      })
    </script>
  </body>
</html>

前端人 精学ahooks源码 走读代码

  1. 创建observer
  2. 监听boxEl的改变 当我们点击按钮的时候,增加box的宽度,当它改变就触发cb回调函数
function useMutationObserver(callback: MutationCallback, target, options: MutationObserverInit = {}) {
  const callbackRef = useLatest(callback)

  useEffectWithTarget(
    () => {
      const el = getTargetElement(target)
      if (!el) return
      // 闭包问题 closure problem
      // const observer = new MutationObserver(callbackRef.current)
      const observer = new MutationObserver((...args) => callbackRef.current(...args))
      observer.observe(el, options)

      return () => {
        observer.disconnect()
      }
    },
    [options],
    target
  )
}

其中MutationObserver接受的回调函数可能被缓存下来了,而在ahooks源码中,存在闭包问题,拿到的是第一次的值,已经提交PR

useInViewport

前置知识

Intersection Observer API

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .wrap {
        width: 150px;
        height: 500px;
        border: 1px solid red;
        overflow: scroll;
      }
      .container {
        width: 150px;
        height: 3000px;
        padding-top: 600px;
      }
      .scroll-area {
        width: 100px;
        height: 300px;
        background: pink;
      }
      .box {
        width: 100px;
        height: 100px;
        background: skyblue;
        margin: -50% 0 -50% 0;
      }
    </style>
  </head>
  <body>
    <div class="wrap">
      <div class="container">
        <div class="scroll-area">111</div>
      </div>
    </div>
    <div class="box"></div>

    <script>
      const options = {
        rootMargin: '0px',
        threshold: 0.1,
      }
      const observer = new IntersectionObserver((e, obs) => {
        for (const entry of e) {
          console.log(entry.isIntersecting)
          console.log(entry.intersectionRatio)
        }
      }, options)

      observer.observe(document.querySelector('.scroll-area'))
    </script>
  </body>
</html>

效果如下:可以监听dom在滚动区域中的状态,比如:可见型、阙值等

前端人 精学ahooks源码

实现

本质:就是拿到需要监听的dom以及IntersectionObserver需要的options传递即可

function useInViewport(target, options: Options = {}) {
  const { callback, ...option } = options

  const [inViewport, setInViewport] = useState<boolean>()
  const [ratio, setRatio] = useState<number>()

  useEffectWithTarget(
    () => {
      const targets = Array.isArray(target) ? target : [target]
      const els = targets.map((target) => getTargetElement(target)).filter((el) => !!el)
      if (!els.length) return

      console.log(els)

      const observer = new IntersectionObserver(
        (entries) => {
          for (const entry of entries) {
            setInViewport(entry.isIntersecting)
            setRatio(entry.intersectionRatio)
            callback?.(entry)
          }
        },
        { ...option, root: getTargetElement(option?.root) }
      )

      els.forEach((el) => {
        observer.observe(el)
      })

      return () => {
        observer.disconnect()
      }
    },
    [options],
    target
  )

  return [inViewport, ratio]
}