useHooks源码分析

一.什么是useHooks?

useHooks,是一款为 React 开发场景而生的 Hooks 库。
官网地址 GitHub地址

1.特性

全新的 useHooks 主要具备以下特性:

  • 🚀 易学易用:直接通过 npm 下载即可使用
  • 🔍 内置全文搜索:标题、正文、demo 等内容均可被搜索,支持多关键词搜索
  • 🎨 包含丰富的基础 Hooks:如 useCreation、useLatest 等基础 hooks 为 react 开发赋能
  • 🚥 可靠的代码健壮:使用 Typescript 构建,提供完善的类型定义文件
  • 💎 完善的文档能力:支持文档记录,支持 demo 演示

2.问题反馈

如果在使用过程中发现任何问题、或者有改善建议,欢迎在 GitHub Issues 进行反馈:issues

3.下载

npm install @csshero/usehooks
# or
yarn add @csshero/usehooks
# or
pnpm add @csshero/usehooks

4.使用示例

只需从 @csshero/usehooks导入你需要的函数

import { useUpdate } from '@csshero/usehooks';
import React from 'react';

export default () => {
  const update = useUpdate();

  return (
    <>
      <div>Time: {Date.now()}</div>
      <button type="button" onClick={update} style={{ marginTop: 8 }}>
        update
      </button>
    </>
  );
};

二.源码分析

1. 高级部分

1.1 useCreation
1.1.1 介绍

useCreationuseMemo 的替代品。

  • 这个hooks是用来替换useMemo,因为useMemo虽然也可以进行数据的缓存,但是它不能保证缓存的值一定不会被重新计算,我记得我在我的useHooks的官网里也提到了 在React官方文档里说 在未来React可能会增加丢弃缓存的特性,useMemo仅仅只是作为性能优化的手段,不然的话使用state或者ref。

  • 意思就是如果你只是为了性能优化而使用 useMemo,那么这种可能的缓存丢弃并不会影响你的业务逻辑;你的代码在没有 useMemo 的情况下也能正常工作,只是性能会稍微差一些。

    但如果你的业务逻辑需要依赖一个“持久稳定”的值——也就是说,这个值在整个组件生命周期内都必须保持不变(除非你主动更新它),那么 useMemo 可能就不合适,因为它可能会被 React 清理掉。这时,更合适的做法是使用 state 变量或者 ref,因为它们不会因为 React 的内部优化策略而意外丢失。

  • useCreation其实就是用Ref包裹了一个对象,这个对象的属性名有dependencies、init和最终结果的obj,每次会判断dependencies是否相同,要是不相同则重新运行factory函数,赋值给返回的obj,然后把新的dependencies赋值给旧的dependencies

1.1.2 源码分析
  • isSameDep 负责判断传递的俩个数组是否相等
const isSameDep = (
  oldDep: React.DependencyList,
  newDep: React.DependencyList,
) => {
  if (oldDep.length !== newDep.length) return false;
  return oldDep.every((item, index) => Object.is(item, newDep[index]));
};

export default isSameDep;
  • 用useRef保证稳定的引用,只要依赖的 deps 没有变化则不改变返回的 factory()
  • init 用于判断是否进行初始化了,默认设置为 false
  • 只有当 init false 或者 依赖的 deps 数组发生改变才会修改 返回的 factory() 值,同时修改 deps 为最新的 deps
import { useRef } from 'react';
import isSameDep from '../utils/isSameDep';

const useCreation = <T>(factory: () => T, deps: React.DependencyList) => {
  const { current } = useRef({
    obj: null as T,
    deps,
    init: false,
  });

  if (!current.init || !isSameDep(current.deps, deps)) {
    current.obj = factory();
    current.deps = deps;
    current.init = true;
  }

  return current.obj;
};

export default useCreation;
1.2 useLatest
1.2.1 介绍

useLatest 用于返回当前最新值的 Hook,可以避免闭包陷阱问题

闭包陷阱产生的原因就是 useEffect 等 hook 里用到了某个 state,但是没有加到 deps 数组里,这样导致 state 变了却没有执行新传入的函数,依然引用的之前的 state。

1.2.2 源码分析
  • 源码非常简单,只需要将传入的 value 值用 useRef 包裹起来

  • 为了保证 valRef.current 始终包含最新的 value,我们在每次渲染时都需要手动更新 valRef.current = value

  • 为什么返回valRef 不直接返回 valRef.current

    • 如果在自定义 hook 内部直接返回 valRef.current,那么每次调用这个 hook 时,你拿到的是那一刻的值快照。虽然在当前渲染中这个值是最新的,但如果你在其他地方(例如异步回调中)捕获了这个返回值,它就固定为那次渲染时的状态,不会随着后续的更新而改变。
    • React 中 useRef 返回的对象在整个组件生命周期中是稳定不变的。返回整个 ref 对象,外部使用者可以在任何时候通过访问 .current 来获取最新的值,而不必担心因捕获值快照而导致数据不更新的问题。
import { useRef } from 'react';

const useLatest = <T>(value: T) => {
  const valRef = useRef(value);
  valRef.current = value;
  return valRef;
};

export default useLatest;
1.3 useMemoizedFn
1.3.1 介绍

持久化 function 的 Hook,一般情况下,可以使用 useMemoizedFn 完全代替 useCallback, 特殊情况见FAQ

在某些场景中,我们需要使用 useCallback 来记住一个函数,但是在第二个参数 deps 变化时,会重新生成函数,导致函数地址变化。

const [state, setState] = useState('');

// 在 state 变化时,func 地址会变化
const func = useCallback(() => {
  console.log(state);
}, [state]);

使用 useMemoizedFn,可以省略第二个参数 deps,同时保证函数地址永远不会变化。

const [state, setState] = useState('');

// func 地址永远不会变化
const func = useMemoizedFn(() => {
  console.log(state);
});
FAQ

useMemoizedFn 返回的函数没有继承 fn 自身的属性?

useMemoizedFn 返回的函数与传入的 fn 的引用完全不同,且没有继承 fn 自身的属性。如果想要持久化后函数自身的属性不丢失,目前 useMemoizedFn 满足不了,请降级使用 useCallbackuseMemo

Related issues: 详情

// useMemoizedFn返回的函数是一个新的函数,只是在新函数中调用了传入的 fn

return function (...args) {

 return fnRef.current.apply(this, args);

};
1.3.2 源码解析
  • 对传递过来的 fn 进行一层 useRef 包裹,为了保证后面 memoizedFn.current 中每次调用时能获取到当前最新的fn
    • 如果不把 fn 用 useRef 包裹,稳定的 memoizedFn 在首次创建时就会闭包捕获那个渲染周期中的 fn 版本,即便后续渲染中 fn 发生了变化,也无法反映最新的逻辑
  • memoizedFn 则是用来 调用需要记忆的函数,memoizedFn.current是一个固定的函数,在这个函数里面 调用了最新的 fn,并且把this 设置为 memoizedFn.current 的 this
import { useMemo, useRef } from 'react';

type FnType = (...args: any[]) => any;
const useMemoizedFn = <T extends FnType>(fn: T): T => {
  const fnRef = useRef<T>(fn);

  fnRef.current = useMemo<T>(() => fn, [fn]);

  const memoizedFn = useRef<FnType>();
  if (!memoizedFn.current) {
    memoizedFn.current = function (...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current as T;
};

export default useMemoizedFn;

2.生命周期部分

2.1 useMount
2.1.1 介绍

只在组件初始化时执行的 Hook

2.1.2 源码解析
  • 将传入的函数放入到 useEffect中,第二个参数为 []。这样只有当前组件被挂起时才会调用
import { useEffect } from 'react';

const useMount = (callback: () => void) => {
  useEffect(() => {
    callback();
  }, []);
};

export default useMount;
2.2 useUnmount
2.2.1 介绍

在组件卸载(unmount)时执行的 Hook

2.2.2 源码解析
  • 使用 useLatest 包裹一下 卸载时执行的fn,保证卸载时执行的fn是最新的
import { useEffect } from 'react';
import useLatest from '../useLatest';

const useUnmount = (callback: () => void) => {
  const CallbackRef = useLatest(callback);

  // 返回值中调用fn的最新值
  useEffect(() => () => CallbackRef.current(), []);
};

export default useUnmount;

3. 副作用部分

3.1 useTimeout
3.1.1 介绍

一个可以处理 setTimeout 计时器函数的 Hook

3.1.2 源码解析
  • 给传递进来的 callback 进行一层useRef嵌套,方便获取到最新的函数,如果不使用useRef嵌套,那放入到setTImeout里要执行的函数则无法执行最新的值,只能执行初始时函数的旧值
  • 给setTimeout传递函数时需要在外侧包裹一层函数,不能直接 setTimeout(callbackRef.current, delay),因为直接传入 callbackRef.current 会在调用 setTimeout 时立即获取当时的函数引用,并固定下来。用() => {} 包裹一层后setTimeout会在delay后执行 () => {} 在这函数里又调用了 callbackRef.current() 因此可以保证调用时,才会去访问最新的callbackRef.current ,直接 setTimeout(callbackRef.current, delay)则相当于在delay直接执行当时传入的函数,因为是先放到宏任务里面
import { useCallback, useEffect, useRef } from 'react';

type FnType = () => void;

const useTimeout = (callback: FnType, delay: number = 1000): FnType => {
  const timer = useRef<ReturnType<typeof setTimeout>>();
  const callbackRef = useRef<FnType>(callback);

  const clearFn = useCallback(() => clearTimeout(timer.current), []);

  // 更新 ref 中的 callback
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  useEffect(() => {
    timer.current = setTimeout(() => {
      callbackRef.current();
    }, delay);
    return clearFn;
  }, [delay]);

  return clearFn;
};

export default useTimeout;
3.2 useInterval
3.2.1 介绍

一个可以处理 setInterval 的 Hook

3.2.2 源码解析
  • 整体实现和 useTimeout 很像 不再过多赘述
import { useCallback, useEffect, useRef } from 'react';

type FnType = () => void;
const useInterval = (
  callback: FnType,
  delay: number = 1000,
  immediate: boolean = false,
): FnType => {
  const timerRef = useRef<ReturnType<typeof setInterval>>();
  const callbackRef = useRef<FnType>(callback); // 用于保存最新的 callback

  const clearFn = useCallback(() => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  }, []);

  // 每次渲染时更新 callbackRef 的值
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  useEffect(() => {
    if (immediate) {
      callbackRef.current();
    }
    timerRef.current = setInterval(() => {
      callbackRef.current(); // 每次都读取最新的 callback
    }, delay);
    return clearFn;
  }, [delay]);

  return clearFn;
};

export default useInterval;
3.3 useUpdate
3.3.1 介绍

useUpdate 会返回一个函数,调用该函数会强制组件重新渲染

3.3.2 源码分析
  • 基于useState实现,每次点击updata函数,都会重新设置 state 为新的空对象
import { useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';

const useUpdate = () => {
  const [, setState] = useState({});
  const updata = useMemoizedFn(() => setState({}));
  return updata;
};

export default useUpdate;
3.4 useDebounceFn
3.4.1 介绍

用来处理防抖函数的 Hook

3.4.2 源码分析
  • 依赖 外部的lodash库,然后根据需要传递对应的参数
import _ from 'lodash-es';
import useCreation from '../useCreation';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';

interface Option {
  wait?: number;
  leading?: boolean;
  trailing?: boolean;
  maxWait?: number;
}
type FnType = (...args: any[]) => any;

const useDebounceFn = (callback: FnType, option: Option) => {
  // 设置默认值
  option.leading = option?.leading ?? false;
  option.trailing = option?.trailing ?? true;
  option.wait = option?.wait ?? 1000;

  const callbackRef = useLatest(callback);

  // 保存_.debounce()返回的函数
  const debounce = useCreation(
    () =>
      _.debounce(
        (...args) => {
          callbackRef.current(...args);
        },
        option.wait,
        option,
      ),
    [],
  );

  useUnmount(() => {
    debounce.cancel();
  });

  return {
    run: debounce,
    cancel: debounce.cancel,
    flush: debounce.flush,
  };
};

export default useDebounceFn;
3.5 useThrottleFn
3.5.1 介绍

用来处理节流函数的 Hook

3.5.2 源码分析
  • 依赖 外部的lodash库,然后根据需要传递对应的参数
import _ from 'lodash-es';
import useCreation from '../useCreation';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';

interface ThrottleOptions {
  wait?: number;
  leading?: boolean;
  trailing?: boolean;
}
interface ThrottledFn<T extends FnType> {
  run: (...args: Parameters<T>) => ReturnType<T>;
  cancel: () => void;
  flush: () => void;
}
type FnType = (...args: any[]) => any;

const useThrottleFn = <T extends FnType>(
  callback: T,
  option: ThrottleOptions,
): ThrottledFn<T> => {
  // 设置默认值
  option.leading = option?.leading ?? true;
  option.trailing = option?.trailing ?? true;
  option.wait = option?.wait ?? 1000;

  const callbackRef = useLatest(callback);

  // 保存_.debounce()返回的函数
  const throttle = useCreation(
    () =>
      _.throttle(
        (...args: Parameters<T>): ReturnType<T> => {
          return callbackRef.current(...args);
        },
        option.wait,
        option,
      ),
    [],
  );

  useUnmount(() => {
    throttle.cancel();
  });

  return {
    run: throttle,
    cancel: throttle.cancel,
    flush: throttle.flush,
  };
};

export default useThrottleFn;

4. 状态部分

4.1 useBoolean
4.1.1 介绍

优雅的管理 boolean 状态的 Hook

4.1.2 源码分析
  • 定义 state 默认为 false
  • 根据不同的方法进行切换
import { useState } from 'react';

interface Actions {
  set: (value: boolean) => void;
  setTrue: () => void;
  setFalse: () => void;
  toggle: () => void;
}

const useBoolean = (defaultValue: boolean = false): [boolean, Actions] => {
  const [value, setValue] = useState(defaultValue);

  const actions: Actions = {
    set: setValue,
    setTrue: () => setValue(true),
    setFalse: () => setValue(false),
    toggle: () => setValue((v) => !v),
  };

  return [value, actions];
};

export default useBoolean;
4.2 useToggle
4.2.1 介绍

用于在两个状态值间切换的 Hook

4.2.2 源码分析
  • 基于函数的重载,来根据传递不同的参数类型,返回不同的结果

  • useBoolean的升级版,根据传递不同的参数,返回不同的结果

import { useMemo, useState } from 'react';

interface Actions<T> {
  set: (value: T) => void;
  setToDefault: () => void;
  setToReverse: () => void;
  toggle: () => void;
}

function useToggle(): [boolean, Actions<boolean>];
function useToggle<T>(defaultValue: T): [T, Actions<T>];
function useToggle<T, R>(defaultValue: T, reverseValue: R): [T, Actions<T | R>];

function useToggle<D, R>(defaultValue: D = false as D, reverseValue?: R) {
  const [value, setValue] = useState<D | R>(defaultValue);
  const actions = useMemo(() => {
    const reverseValueOrigin = (
      reverseValue === undefined ? !defaultValue : reverseValue
    ) as D | R;

    const toggle = () => {
      setValue((v) => (v === defaultValue ? reverseValueOrigin : defaultValue));
    };
    const setToDefault = () => setValue(defaultValue);
    const setToReverse = () => setValue(reverseValueOrigin);
    const set = (v: D | R) => setValue(v);

    return { toggle, setToDefault, setToReverse, set };
  }, []);

  return [value, actions];
}

export default useToggle;
4.3 useThrottleValue
4.3.1 介绍

用来对 处理节流的 Hook

4.3.2 源码分析
  • 将外部传递进来的值作为useState的初始值
  • 每次当外部传递的value变化时,都会触发useThrottleFn的run方法
import { useEffect, useState } from 'react';
import useThrottleFn from '../useThrottleFn';

interface ThrottleOptions {
  wait?: number;
  leading?: boolean;
  trailing?: boolean;
}

const useThrottleValue = <T>(value: T, options: ThrottleOptions) => {
  const [throttled, setThrottled] = useState(value);

  const { run } = useThrottleFn(() => {
    setThrottled(value);
  }, options);

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

  return throttled;
};

export default useThrottleValue;
4.4 useDebounceValue
4.4.1 介绍

用来对 处理防抖的 Hook

4.4.2 源码分析
  • 将外部传递进来的值作为useState的初始值
  • 每次当外部传递的value变化时,都会触发useDebounceFn的run方法
import { useEffect, useState } from 'react';
import useDebounceFn from '../useDebounceFn';

interface Option {
  wait?: number;
  leading?: boolean;
  trailing?: boolean;
  maxWait?: number;
}
const useDebounceValue = <T>(value: T, options: Option) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

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

  return debouncedValue;
};

export default useDebounceValue;
4.5 useMap
4.5.1 介绍

管理 Map 类型状态的 Hook

4.5.2 使用的主要事项

在给 useMap 传递参数时 因为参数的类型是个可以迭代的集合,其中每个元素都是一个包含键和值的二元元组。但是在默认情况下,TypeScript 会把写的元组推断为数组类型

const test = ['a', 'b']; // test: string[]

因此需要给传递的参数进行声明

const data: [string, string][] = [['a', 'b']];
const [map, { set, setAll, remove, reset, get }] = useMap<string, string>(data);
const [map, { set, setAll, remove, reset, get }] = useMap<string, string>([
  ['msg', 'hello world'],
  ['info', 'hello react'],
] as const);

以上俩种都可以传递正确的参数类型

4.5.3 源码分析
  • 接收的参数必须是一个可迭代对象,其每个元素都应当是一个长度为 2 的数组,表示一个键值对
  • 每次操作都需要新建一个temp Map,因为React的数据不可变性
    • 直接修改 prev 会导致对原有状态的副作用,可能会在其他依赖同一状态的地方引发难以察觉的 bug。通过复制一份 Map(即用 temp),再进行修改,可以保证每次更新返回的是一个全新的对象,这符合不可变更新的最佳实践。
import { useState } from 'react';
import useCreation from '../useCreation';

interface Actions<K, V> {
  set: (key: K, value: V) => void;
  get: (key: K) => V | undefined;
  remove: (key: K) => void;
  reset: () => void;
  setAll: (newMap: Iterable<readonly [K, V]>) => void;
}

const useMap = <K, V>(
  initValue?: Iterable<readonly [K, V]>,
): [Map<K, V>, Actions<K, V>] => {
  const initMap = () => new Map(initValue);
  const [map, setMap] = useState<Map<K, V>>(initMap);

  const actions = useCreation(() => {
    const set = (key: K, value: V) => {
      setMap((prev) => {
        const temp = new Map(prev);
        temp.set(key, value);
        return temp;
      });
    };
    const get = (key: K) => map.get(key);
    const remove = (key: K) => {
      setMap((prev) => {
        const temp = new Map(prev);
        temp.delete(key);
        return temp;
      });
    };
    const reset = () => setMap(initMap());
    const setAll = (newMap: Iterable<readonly [K, V]>) => {
      setMap(new Map(newMap));
    };
    return {
      set,
      get,
      remove,
      reset,
      setAll,
    };
  }, []);

  return [map, actions];
};

export default useMap;
4.6 useSet
4.6.1 介绍

管理 Set 类型状态的 Hook

4.6.2 源码分析
  • 和useMap很像,不再赘述了
import { useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';

function useSet<K>(initialValue?: Iterable<K>) {
  const getInitValue = () => new Set(initialValue);
  const [set, setSet] = useState<Set<K>>(getInitValue);

  const add = (key: K) => {
    if (set.has(key)) {
      return;
    }
    setSet((prevSet) => {
      const temp = new Set(prevSet);
      temp.add(key);
      return temp;
    });
  };

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

  const reset = () => setSet(getInitValue());

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

export default useSet;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值