前端人 精学ahooks源码

背景

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

本文将渐进式更新...

前置知识

前言

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

状态就像快照

ts 复制代码
  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 返回当前最新的值
ts 复制代码
function useLatest<T>(value: T) {
  const ref = useRef(value)
  ref.current = value

  return ref
}
  • 解决闭包问题
ts 复制代码
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不会
ts 复制代码
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

  • 忽略首次执行,只有依赖发生改变才会执行
ts 复制代码
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

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

useUnmount

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

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

useUnmountedRef

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

State

useSetState

  • 案例说明
tsx 复制代码
  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重新定义(引用不同,如果传递给子组件,导致子组件进行不必要的执行)
ts 复制代码
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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
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

ts 复制代码
function useGetState(initialState) {
  const [state, setState] = useState(initialState)
  const latestRef = useLatest(state)

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

  return [state, setState, getState]
}
  • 获取最新的state

useResetState

ts 复制代码
function useResetState(initialState) {
  const [state, setState] = useState(initialState)

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

  return [state, setState, resetState]
}
  • 利用useCallback, 每次当hook重新执行,而resetState中的回调函数拿到的还是初始化的值,利用闭包
相关推荐
忆江南2 分钟前
# Flutter 语音房礼物下载方案(完整版)
前端
悟空瞎说3 分钟前
React 19 带来了诸多创新
前端·react.js
im_AMBER6 分钟前
高并发下的列表乱序与文档同步
前端·react.js·架构
Java面试题总结26 分钟前
2026Java面试八股文合集(持续更新)
java·spring·面试·职场和发展·java面试·java八股文
前进的李工34 分钟前
LangChain使用之Model IO(提示词模版之ChatPromptTemplate)
java·前端·人工智能·python·langchain·大模型
漫随流水1 小时前
旅游推荐系统(login.html)
前端·html·旅游
1024小神1 小时前
记录xcode项目swiftui配置APP加载启动图
前端·ios·swiftui·swift
城沐小巷1 小时前
【无标题】
面试·职场和发展·毕业设计·课程设计·毕设
CHU7290351 小时前
社区生鲜买菜小程序前端功能版块设计及玩法介绍
前端·小程序
尤山海1 小时前
深度防御:内容类网站如何有效抵御 SQL 注入与脚本攻击(XSS)
前端·sql·安全·web安全·性能优化·状态模式·xss