$ npm install --save ahooks
# or
$ yarn add ahooks
import { useRequest } from 'ahooks';
适用于对时间精度要求不高的情况,要求高的话还是需要结合服务器时间来判断。
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]);
适用于一些异步操作的场景,比如对请求的后续操作,要判断当前页面是否还在。
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;
useMount(() => {
//页面初始化时滑到最顶端
window.scrollTo(0, 0);
});
const useMount = (fn: () => void) => {
...
useEffect(() => {
fn?.();
}, []);
};
//组件卸载时执行
useUnmount(() => {
dispatch({
type: 'purchase/changeApproModal',
payload: false,
});
});
const useUnmount = (fn: () => void) => {
...
const fnRef = useLatest(fn);
useEffect(
() => () => {
fnRef.current();
},
[],
);
};
//设置定时器,返回值是一个清除定时器的方法
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;
export const createUpdateEffect =
(hook) => (effect, deps) => {
//标记是否已初始化
const isMounted = useRef(false);
...
//hook就是react.useLayoutEffect
hook(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
//只有当组件已初始化过后才会执行
return effect();
}
}, deps);
};
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],
);
}
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,
);
}
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;
如果搞不清楚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);
方法