aHooks简单总结

一套高质量可靠的 React Hooks 库

特性

  • 易学易用 支持 SSR(服务端渲染)
  • 对输入输出函数做了特殊处理,避免闭包问题(解决内存占用,优化网页性能)
  • 包含大量提炼自业务的高级
  • Hooks 包含丰富的基础 Hooks(易于学习理解)
  • 使用 TypeScript构建,提供完整的类型定义文件(开发工具中出现提示,提升开发效率)

安装

$ npm install --save ahooks
# or
$ yarn add ahooks

使用

import { useRequest } from 'ahooks';

hooks分类

  • useRequest(统一处理请求的方式,底层使用Fetch请求,支持轮询、刷新、防抖、节流、错误重试等,封装过强,不适用于目前的项目)
  • Scene,业务场景类Hooks,结合Antd,封装了一些固定场景(翻页、虚拟化列表、计数、无限滚动)下可能会用到的hook
    • useCountDown
  • LifeCycle,管理组件生命周期的Hooks,比较有用
    • useUnmountedRef
    • useMount
    • useUnmount
  • State,管理State的Hooks
  • Effect,对基础的Effect进行扩展,使其功能更丰富
    • useInterval
    • useUpdateLayoutEffect
    • useLockFn
  • Dom,处理Dom操作的Hooks,对于Dom操作不太熟悉的开发可以有效提高开发效率
    • useClickAway
    • useScroll
  • Advanced,进阶Hooks
    • useMemoizedFn
  • Dev,适用于开发阶段的Hooks,帮助提升开发效率

用过的Hooks

  • useMemoizedFn
  • useUnmountedRef
  • useMount
  • useClickAway
  • useInterval
  • useCountDown
  • useUpdateLayoutEffect
  • useLockFn
  • useUnmount
  • useScroll

1.useCountDown,一个用于管理倒计时的 Hook。

适用于对时间精度要求不高的情况,要求高的话还是需要结合服务器时间来判断。

应用场景:进入页面,默认打开弹窗,5秒后关闭弹窗。

  const [popoverVisible, setPopoverVisible] = useState(true);

  useCountDown({ leftTime: 5 * 1000, onEnd: () => {
    setPopoverVisible(false);
  }, });

主要源码:

const { targetDate, interval = 1000, onEnd } = options || {};
//剩余毫秒数,calcLeft:计算与当前时间的毫秒差值的方法
const [timeLeft, setTimeLeft] = useState(() => calcLeft(targetDate));
//onEnd方法的引用,useLatest返回当前最新值的 Hook,可以避免闭包问题。
const onEndRef = useLatest(onEnd);

useEffect(() => {
    ...
  // 立即执行一次
  setTimeLeft(calcLeft(targetDate));
  //实际还是使用setInterval
  const timer = setInterval(() => {
    const targetLeft = calcLeft(targetDate);
    setTimeLeft(targetLeft);
    if (targetLeft === 0) {
      clearInterval(timer);
      onEndRef.current?.();
    }
  }, interval);
  //组件销毁的同时销毁计数器,释放资源
  return () => clearInterval(timer);
}, [targetDate, interval]);

2.useUnmountedRef,获取当前组件是否已经卸载的 Hook。

适用于一些异步操作的场景,比如对请求的后续操作,要判断当前页面是否还在。

应用场景:


const unUnMountedRef = useUnmountedRef();
 
 //需求:fetchTagsInfo接口调用成功后再调用fetchOnWayTradeOrder
fetchTagsInfo().then(() => {
  //如果此时页面切换,组件已卸载,则无需调用后续接口
  if (unUnMountedRef.current) return;
  fetchOnWayTradeOrder();
});

源码:

const unmountedRef = useRef(false);
  useEffect(() => {
    unmountedRef.current = false;
    return () => {
    //实际就是在useEffect的卸载方法中修改标记unmountedRef    
      unmountedRef.current = true;
    };
  }, []);
  return unmountedRef;

3.useMount,只在组件初始化时执行的 Hook。

应用场景:

  useMount(() => {
      //页面初始化时滑到最顶端
    window.scrollTo(0, 0);
  });

源码:

const useMount = (fn: () => void) => {
  ...    

  useEffect(() => {
    fn?.();
  }, []);
};

4.useUnmount,在组件卸载(unmount)时执行的 Hook。

应用场景:

//组件卸载时执行
useUnmount(() => {
  dispatch({
    type: 'purchase/changeApproModal',
    payload: false,
  });
});

源码:

const useUnmount = (fn: () => void) => {
  ...
  const fnRef = useLatest(fn);

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

5.useInterval,一个可以处理 setInterval 的 Hook。

应用场景:定时查询服务器时间

//设置定时器,返回值是一个清除定时器的方法
const interval = useInterval(() => {
  fetchServerTime();
}, 1000 * 60);
//销毁定时器
useUnmount(() => {
  if (interval && typeof interval === 'function') {
    interval();
  }
});

源码:

const fnRef = useLatest(fn);
const timerRef = useRef(null);

useEffect(() => {
   ...
   //使用setInterval定义一个定时器
  timerRef.current = setInterval(() => {
    fnRef.current();
  }, delay);
  //组件销毁时自动销毁定时器
  return () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  };
}, [delay]);

//返回一个销毁定时器的方法,用于手动销毁
const clear = useCallback(() => {
  if (timerRef.current) {
    clearInterval(timerRef.current);
  }
}, []);

return clear;

6.useUpdateLayoutEffect,用法等同于 useLayoutEffect,但是会忽略首次执行,只在依赖更新时执行。

源码:

export const createUpdateEffect =
  (hook) => (effect, deps) => {
      //标记是否已初始化
    const isMounted = useRef(false);
    ...
    //hook就是react.useLayoutEffect
    hook(() => {
      if (!isMounted.current) {
        isMounted.current = true;
      } else {
          //只有当组件已初始化过后才会执行
        return effect();
      }
    }, deps);
  };

7.useLockFn,用于给一个异步函数增加竞态锁,防止并发执行。

源码:

function useLockFn(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);
        //执行完,解锁
        lockRef.current = false;
        return ret;
      } catch (e) {
          //出异常,解锁
        lockRef.current = false;
        throw e;
      }
    },
    [fn],
  );
}

8.useClickAway,监听目标元素外的点击事件。

export default function useClickAway(
  onClickAway: (event: T) => void,    //触发函数
  target: BasicTarget | BasicTarget[],    //DOM 节点或者 Ref,支持数组
  eventName: string | string[] = 'click',    //指定需要监听的事件,支持数组
) {
  const onClickAwayRef = useLatest(onClickAway);

  useEffectWithTarget(
    () => {
      //3.精髓在于如何处理事件
      const handler = (event: any) => {
        const targets = Array.isArray(target) ? target : [target];
        if (
          targets.some((item) => {
            const targetElement = getTargetElement(item);
            //没有找到指定的dom,或者指定的dom包含了事件发生的target,也就是事件发生在指定的dom,就不会执行后续方法
            return !targetElement || targetElement.contains(event.target);
          })
        ) {
          return;
        }
        //判断事件是否发生在其他dom对象上,是的话就调用onClickAway方法
        onClickAwayRef.current(event);
      };
      //1.找到需要绑定事件的DOM节点,判断当前根节点是否是document或者影子root,可以理解为DOM中的DOM?
      const documentOrShadow = getDocumentOrShadow(target);
      //可以传入多个事件
      const eventNames = Array.isArray(eventName) ? eventName : [eventName];
      //2.绑定多个事件
      eventNames.forEach((event) => documentOrShadow.addEventListener(event, handler));

      return () => {
        eventNames.forEach((event) => documentOrShadow.removeEventListener(event, handler));
      };
    },
    Array.isArray(eventName) ? eventName : [eventName],
    target,
  );
}

9.useScroll,监听元素的滚动位置。

const [position, setPosition] = useRafState<Position>();
useEffectWithTarget(() => {
    //target不传就是document
    const el = getTargetElement(target, document);
    ...
    const updatePosition = () => {
      let newPosition;
      if (el === document) {
        //scrollingElement返回滚动文档的 Element 对象的引用。在标准模式下,这是文档的根元素, document.documentElement。
         newPosition = {
            left: document.scrollingElement.scrollLeft,
            top: document.scrollingElement.scrollTop,
          }; 
        ...
      } else {
        newPosition = {
          left: (el as Element).scrollLeft,
          top: (el as Element).scrollTop,
        };
      }
      setPosition(newPosition);
    };
    updatePosition();
    el.addEventListener('scroll', updatePosition);
    ...
  }, [], target);

return position;

10.useMemoizedFn,持久化 function 的 Hook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。

如果搞不清楚useCallback的用法,建议不要用useMemoizedFn。
在某些场景中,我们需要使用 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);
});

示例:

import { useMemoizedFn } from 'ahooks';
import { message } from 'antd';
import React, { useCallback, useMemo, useRef, useState } from 'react';

export default () => {
  const [count, setCount] = useState(0);

  const callbackFn = useCallback(() => {
    message.info(`Current count is ${count}`);
  }, [count]);

  const memoizedFn = useMemoizedFn(() => {
    message.info(`Current count is ${count}`);
  });

  const normalFn = () => {
    message.info(`Current count is ${count}`);
  }

  return (
    <>
      <p>count: {count}</p>
      <button
        type="button"
        onClick={() => {
          setCount((c) => c + 1);
        }}
      >
        Add Count
      </button>

      <p>可以单击该按钮以查看子组件渲染的数量</p>

      <div style={{ marginTop: 32 }}>
        <h3>具有useCallback函数的组件:</h3>
        {/* use callback function, ExpensiveTree component will re-render on state change */}
        {/* {
          useMemo(()=> , [callbackFn])
        } */}
        <ExpensiveTree showCount={callbackFn} />
      </div>

      <div style={{ marginTop: 32 }}>
        <h3>具有useMemoizedFn函数的组件:</h3>
        {/* use memoized function, ExpensiveTree component will only render once */}
        {/* {
          useMemo(()=> , [memoizedFn])
        } */}
        <ExpensiveTree showCount={memoizedFn} />
      </div>

      <div style={{ marginTop: 32 }}>
        <h3>普通方法:</h3>
        {/* {
          useMemo(()=> , [normalFn])
        } */}
        <ExpensiveTree showCount={normalFn} />
      </div>
    </>
  );
};

// some expensive component with React.memo
const ExpensiveTree = React.memo(({ showCount }) => {
  const renderCountRef = useRef(0);
  renderCountRef.current += 1;

  return (
    <div>
      <p>子组件渲染次数: {renderCountRef.current}</p>
      <button type="button" onClick={showCount}>
        showParentCount
      </button>
    </div>
  );
});

源码就不讲了,比较绕,关键在于fnRef.current.apply(this, args);方法

与react-use的对比

aHooks简单总结_第1张图片
aHooks简单总结_第2张图片
aHooks简单总结_第3张图片
aHooks简单总结_第4张图片
从star数量和下载量来看都是react-use占优,以后可以考虑使用react-use。

你可能感兴趣的:(javascript,前端,react)