useUpdateEffect、useUpdateLayoutEffect
这两个 hook 在之前的文章里面说过,不过还是拿出来再走一遍吧
//入口函数为 createUpdateEffect//useUpdateEffect 传入 useEffectexport default createUpdateEffect(useEffect);//useUpdateLayoutEffect 传入 useLayoutEffectexport default createUpdateEffect(useLayoutEffect);//代码实现export const createUpdateEffect: (hook: effectHookType,) => effectHookType = hook => (effect, deps) => {const isMounted = useRef(false);// for react-refreshhook(() => {return () => {isMounted.current = false;};}, []);hook(() => {// 首次执行完时候,设置为 true,从而下次依赖更新的时候可以执行逻辑if (!isMounted.current) {isMounted.current = true;} else {return effect();}}, deps);};//入口函数为 createUpdateEffect //useUpdateEffect 传入 useEffect export default createUpdateEffect(useEffect); //useUpdateLayoutEffect 传入 useLayoutEffect export default createUpdateEffect(useLayoutEffect); //代码实现 export const createUpdateEffect: ( hook: effectHookType, ) => effectHookType = hook => (effect, deps) => { const isMounted = useRef(false); // for react-refresh hook(() => { return () => { isMounted.current = false; }; }, []); hook(() => { // 首次执行完时候,设置为 true,从而下次依赖更新的时候可以执行逻辑 if (!isMounted.current) { isMounted.current = true; } else { return effect(); } }, deps); };//入口函数为 createUpdateEffect //useUpdateEffect 传入 useEffect export default createUpdateEffect(useEffect); //useUpdateLayoutEffect 传入 useLayoutEffect export default createUpdateEffect(useLayoutEffect); //代码实现 export const createUpdateEffect: ( hook: effectHookType, ) => effectHookType = hook => (effect, deps) => { const isMounted = useRef(false); // for react-refresh hook(() => { return () => { isMounted.current = false; }; }, []); hook(() => { // 首次执行完时候,设置为 true,从而下次依赖更新的时候可以执行逻辑 if (!isMounted.current) { isMounted.current = true; } else { return effect(); } }, deps); };
这两个 hook 都通过 createUpdateEffect
入口函数,只不过一个传 useEffect
,一个传 useLayoutEffect
作为参数。他们和 useEffect
、useLayoutEffect
的用法一样,只不过这两个 hook 会忽略首次执行
思路:
- 内部通过
useRef
初始化一个标识isMounted
,用来判断组件是否挂载 - 然后执行
hook
(也就是useEffect
、useLayoutEffect
),当组件首次挂载时,会执行更改标识isMounted.current = true
,也就是忽略了首次执行 - 当依赖发生改变时,在执行回调
effect
useAsyncEffect
useAsyncEffect
支持传入一个异步的 callback
我们知道 useEffect 是不允许传入异步的 callback 的,比如:
useEffect(async () => {await fetch(url)}, [])useEffect(async () => { await fetch(url) }, [])useEffect(async () => { await fetch(url) }, [])
这样写是会报错的,因为 useEffect 会在重新渲染时,清除上一次的副作用,或者在组件销毁时,清除副作用,它应该返回一个 cleanup
函数,而不是返回 Promise,这会导致 React 在调用销毁函数的时候报错,因为当返回值是异步时,无法预知代码的执行情况,所以可能导致一些难以发现的 bug
从源码的角度来看这个问题的话:
function commitHookEffectListUnmount(tag, finishedWork) {var updateQueue = finishedWork.updateQueue;var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;if (lastEffect !== null) {var firstEffect = lastEffect.next;var effect = firstEffect;do {if ((effect.tag & tag) === tag) {// Unmountvar destroy = effect.destroy;effect.destroy = undefined;// 这里!!!!if (destroy !== undefined) {destroy();}}effect = effect.next;} while (effect !== firstEffect);}}function commitHookEffectListUnmount(tag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Unmount var destroy = effect.destroy; effect.destroy = undefined; // 这里!!!! if (destroy !== undefined) { destroy(); } } effect = effect.next; } while (effect !== firstEffect); } }function commitHookEffectListUnmount(tag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Unmount var destroy = effect.destroy; effect.destroy = undefined; // 这里!!!! if (destroy !== undefined) { destroy(); } } effect = effect.next; } while (effect !== firstEffect); } }
从源码中可以看出,卸载的时候会通过函数组件对应的 fiber 获取 effect 链表,然后遍历链表,获取环链上的每一个节点,如果 destroy 不是 undefined 就执行,所以如果 useEffect 第一个参数传入 async, 那么这里的 destroy 就是一个 promise 对象,对象是不能执行的,所以报错。
那我们如果非要在 useEffect 里面使用异步函数怎么办呢?两种方式
- 方式一:定义一个函数用来接受异步函数返回的 Promise
useEffect(() => {const asyncFun = async () => {//do something};asyncFun();}, []);
useEffect(() => { const asyncFun = async () => { //do something }; asyncFun(); }, []);
useEffect(() => { const asyncFun = async () => { //do something }; asyncFun(); }, []);
- 方式二:立即执行函数
useEffect(() => {(async () => {// do something})();}, []);
useEffect(() => { (async () => { // do something })(); }, []);
useEffect(() => { (async () => { // do something })(); }, []);
但是在看 useAsyncEffect 的源码前,我们先来认识一下 Gnerator
Gnerator
Generator
是一个返回值 iterator
对象的函数。而 iterator
叫做迭代器,它要满足迭代器协议:一个拥有 next
方法的对象,且 next
方法要返回形如 {done: boolean, value: any}
的对象,其中 done
用来表示当前的数据结构是否遍历完毕,value
是每一次遍历的值
那 iterator
有什么用?我们知道,像 Array 这样的数据结构我们是可以去遍历、拓展的,因为它默认实现了 Symbol.iterator
属性,该属性如果被定义了,就可以被遍历、拓展,比如:
let arr = ['a','b','c'];let iter = arr[Symbol.iterator]();iter.next() // { value: 'a', done: false }iter.next() // { value: 'b', done: false }iter.next() // { value: 'c', done: false }iter.next() // { value: undefined, done: true }let arr = ['a','b','c']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true }let arr = ['a','b','c']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true }
我们手动通过为数组 arr 定义 Symbol.iterator
属性,是个函数,调用它时会返回迭代器,也就是 iter
,然后通过 next
方法,就可以遍历 arr 了
对于对象来说也是如此:
let obj = {a: "hello", b: "world"};// 自定义迭代器function createIterator(items) {let keys = Object.keys(items);let i = 0;return {next: function () {let done = (i >= keys.length);let value = !done ? items[keys[i++]] : undefined;return {value: value,done: done,};}};}let iterator = createIterator(obj); // 返回迭代器console.log(iterator.next()); // "{ value: 'hello', done: false }"console.log(iterator.next()); // "{ value: 'world', done: false }"console.log(iterator.next()); // "{ value: undefined, done: true }"let obj = {a: "hello", b: "world"}; // 自定义迭代器 function createIterator(items) { let keys = Object.keys(items); let i = 0; return { next: function () { let done = (i >= keys.length); let value = !done ? items[keys[i++]] : undefined; return { value: value, done: done, }; } }; } let iterator = createIterator(obj); // 返回迭代器 console.log(iterator.next()); // "{ value: 'hello', done: false }" console.log(iterator.next()); // "{ value: 'world', done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }"let obj = {a: "hello", b: "world"}; // 自定义迭代器 function createIterator(items) { let keys = Object.keys(items); let i = 0; return { next: function () { let done = (i >= keys.length); let value = !done ? items[keys[i++]] : undefined; return { value: value, done: done, }; } }; } let iterator = createIterator(obj); // 返回迭代器 console.log(iterator.next()); // "{ value: 'hello', done: false }" console.log(iterator.next()); // "{ value: 'world', done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }"
我们再回到 Generator
来。刚刚说了,Generator 是用于返回 iterator 迭代器的函数,它的定义如下:
//Generator 定义时要在 function 后加一个星号function* generator() {yield 111yield 222};let iterator = generator(); //返回迭代器iterator.next() // {value: 111, done: false}iterator.next() // {value: 222, done: false}iterator.next() // {value: undefined, done: true}//Generator 定义时要在 function 后加一个星号 function* generator() { yield 111 yield 222 }; let iterator = generator(); //返回迭代器 iterator.next() // {value: 111, done: false} iterator.next() // {value: 222, done: false} iterator.next() // {value: undefined, done: true}//Generator 定义时要在 function 后加一个星号 function* generator() { yield 111 yield 222 }; let iterator = generator(); //返回迭代器 iterator.next() // {value: 111, done: false} iterator.next() // {value: 222, done: false} iterator.next() // {value: undefined, done: true}
Generator
内部通过关键字 yield
,可以像 打断点
一样,暂停函数的执行,并且可以从上一次暂停的位置继续往后执行,因此 Generator 可以返回一系列的值,然后像”同步“代码一样执行
这里还有个 Generator 的孪生兄弟,叫做 Symbol.asyncIterator
,它两差不多,只不过后者用来指定一个对象的异步迭代器
,可用于 for await ... of
循环。比如:
const myAsyncIterable = new Object();myAsyncIterable[Symbol.asyncIterator] = async function*() {yield "hello";yield "async";yield "iteration!";};(async () => {for await (const x of myAsyncIterable) {console.log(x);// expected output:// "hello"// "async"// "iteration!"}})();const myAsyncIterable = new Object(); myAsyncIterable[Symbol.asyncIterator] = async function*() { yield "hello"; yield "async"; yield "iteration!"; }; (async () => { for await (const x of myAsyncIterable) { console.log(x); // expected output: // "hello" // "async" // "iteration!" } })();const myAsyncIterable = new Object(); myAsyncIterable[Symbol.asyncIterator] = async function*() { yield "hello"; yield "async"; yield "iteration!"; }; (async () => { for await (const x of myAsyncIterable) { console.log(x); // expected output: // "hello" // "async" // "iteration!" } })();
看到这里,对 Generator 有了个基本的认识,接下来就来看 useAsyncEffect 源码
useAsyncEffect 源码
function isAsyncGenerator(val: AsyncGenerator<void, void, void> | Promise<void>,): val is AsyncGenerator<void, void, void> {return isFunction(val[Symbol.asyncIterator]);}function useAsyncEffect(effect: () => AsyncGenerator<void, void, void> | Promise<void>,deps?: DependencyList,) {useEffect(() => {const e = effect();let cancelled = false;async function execute() {if (isAsyncGenerator(e)) {while (true) {const result = await e.next();if (result.done || cancelled) {break;}}} else {await e;}}execute();return () => {cancelled = true;};}, deps);}export default useAsyncEffect;function isAsyncGenerator( val: AsyncGenerator<void, void, void> | Promise<void>, ): val is AsyncGenerator<void, void, void> { return isFunction(val[Symbol.asyncIterator]); } function useAsyncEffect( effect: () => AsyncGenerator<void, void, void> | Promise<void>, deps?: DependencyList, ) { useEffect(() => { const e = effect(); let cancelled = false; async function execute() { if (isAsyncGenerator(e)) { while (true) { const result = await e.next(); if (result.done || cancelled) { break; } } } else { await e; } } execute(); return () => { cancelled = true; }; }, deps); } export default useAsyncEffect;function isAsyncGenerator( val: AsyncGenerator<void, void, void> | Promise<void>, ): val is AsyncGenerator<void, void, void> { return isFunction(val[Symbol.asyncIterator]); } function useAsyncEffect( effect: () => AsyncGenerator<void, void, void> | Promise<void>, deps?: DependencyList, ) { useEffect(() => { const e = effect(); let cancelled = false; async function execute() { if (isAsyncGenerator(e)) { while (true) { const result = await e.next(); if (result.done || cancelled) { break; } } } else { await e; } } execute(); return () => { cancelled = true; }; }, deps); } export default useAsyncEffect;
它的思路为:
- 先执行传入的
effect()
异步回调函数,将其的返回值赋值给e
- 然后通过内部
isAsyncGenerator
方法 判断e
是否是异步Generator
,如果是,就通过可迭代对象的next
方法执行,直到done: false
完毕为止;如果不是,那就是个Promise
,直接await e
- 最后,内部定义了一个
cancelled
变量,用来中断执行
的
因为可能会遇到一些场景:用户在执行 a 操作,但是 a 操作还没完成时,就开始下一轮的 b 操作,那此时 a 操作是没有意义的,可以直接停止往后执行。直接跳过 a 到 b 操作去。这个 cancelled
就是干这个事情的
useUpdate
这个 hook 之前的文章也讲过,这里就一笔带过
useUpdate
会返回一个函数,调用该函数会强制组件重新渲染
import { useCallback, useState } from 'react';const useUpdate = () => {const [, setState] = useState({});return useCallback(() => setState({}), []);};export default useUpdate;import { useCallback, useState } from 'react'; const useUpdate = () => { const [, setState] = useState({}); return useCallback(() => setState({}), []); }; export default useUpdate;import { useCallback, useState } from 'react'; const useUpdate = () => { const [, setState] = useState({}); return useCallback(() => setState({}), []); }; export default useUpdate;
很简单,就是当我们调用返回的函数时,useUpdate
内部会通过 setState
更改状态,然后就促使组件重新渲染
useDebounceEffect
该 hook 为 useEffect
增加防抖的能力。
function useDebounceEffect(effect: EffectCallback,deps?: DependencyList,options?: DebounceOptions,) {// 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调const [flag, setFlag] = useState({});// 通过 lodash 的 防抖对 setFlag 包装一层防抖效果const { run } = useDebounceFn(() => {setFlag({});}, options);// 首次执行useEffect(() => {// 每当过了 options.wait 时间后,设置 flag 改变标识return run();}, deps);// 当标识改变时,执行 effect 回调函数,但忽略首次执行useUpdateEffect(effect, [flag]);}export default useDebounceEffect;function useDebounceEffect( effect: EffectCallback, deps?: DependencyList, options?: DebounceOptions, ) { // 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调 const [flag, setFlag] = useState({}); // 通过 lodash 的 防抖对 setFlag 包装一层防抖效果 const { run } = useDebounceFn(() => { setFlag({}); }, options); // 首次执行 useEffect(() => { // 每当过了 options.wait 时间后,设置 flag 改变标识 return run(); }, deps); // 当标识改变时,执行 effect 回调函数,但忽略首次执行 useUpdateEffect(effect, [flag]); } export default useDebounceEffect;function useDebounceEffect( effect: EffectCallback, deps?: DependencyList, options?: DebounceOptions, ) { // 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调 const [flag, setFlag] = useState({}); // 通过 lodash 的 防抖对 setFlag 包装一层防抖效果 const { run } = useDebounceFn(() => { setFlag({}); }, options); // 首次执行 useEffect(() => { // 每当过了 options.wait 时间后,设置 flag 改变标识 return run(); }, deps); // 当标识改变时,执行 effect 回调函数,但忽略首次执行 useUpdateEffect(effect, [flag]); } export default useDebounceEffect;
思路如下:
- 设置
flag
标识符变量,然后对setFlag
函数做一个防抖(用的是 lodash 的防抖包装了一层) - 首次执行时不影响
- 后续执行时,当
flag
通过options.wait
时间改变后,才执行useUpdateEffect
执行effect
回调。注意:useUpdateEffect
忽略首次执行,所以代码前面用的是 useEffect
useThrottleEffect
为 useEffect
增加节流的能力。
它也是通过 lodash.throttle 方法来实现的
function useThrottleEffect(effect: EffectCallback,deps?: DependencyList,options?: ThrottleOptions,) {// 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调const [flag, setFlag] = useState({});// 用来处理函数节流的 Hook。const { run } = useThrottleFn(() => {setFlag({});}, options);useEffect(() => {return run();}, deps);// 只有在 flag 变化的时候,才执行 effect 函数useUpdateEffect(effect, [flag]);}function useThrottleEffect( effect: EffectCallback, deps?: DependencyList, options?: ThrottleOptions, ) { // 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调 const [flag, setFlag] = useState({}); // 用来处理函数节流的 Hook。 const { run } = useThrottleFn(() => { setFlag({}); }, options); useEffect(() => { return run(); }, deps); // 只有在 flag 变化的时候,才执行 effect 函数 useUpdateEffect(effect, [flag]); }function useThrottleEffect( effect: EffectCallback, deps?: DependencyList, options?: ThrottleOptions, ) { // 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调 const [flag, setFlag] = useState({}); // 用来处理函数节流的 Hook。 const { run } = useThrottleFn(() => { setFlag({}); }, options); useEffect(() => { return run(); }, deps); // 只有在 flag 变化的时候,才执行 effect 函数 useUpdateEffect(effect, [flag]); }
思路和 useDebounceEffect
一样
useDebounceFn
useDebounceFn
是用来处理防抖函数的 hook
它主要也是使用的 lodash 的 debounce 方法
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {const fnRef = useLatest(fn);const wait = options?.wait ?? 1000;const debounced = useMemo(() =>debounce((...args: Parameters<T>): ReturnType<T> => {return fnRef.current(...args);},wait,options,),[],);useUnmount(() => {debounced.cancel();});return {run: debounced,cancel: debounced.cancel,flush: debounced.flush,};}export default useDebounceFn;function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) { const fnRef = useLatest(fn); const wait = options?.wait ?? 1000; const debounced = useMemo( () => debounce( (...args: Parameters<T>): ReturnType<T> => { return fnRef.current(...args); }, wait, options, ), [], ); useUnmount(() => { debounced.cancel(); }); return { run: debounced, cancel: debounced.cancel, flush: debounced.flush, }; } export default useDebounceFn;function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) { const fnRef = useLatest(fn); const wait = options?.wait ?? 1000; const debounced = useMemo( () => debounce( (...args: Parameters<T>): ReturnType<T> => { return fnRef.current(...args); }, wait, options, ), [], ); useUnmount(() => { debounced.cancel(); }); return { run: debounced, cancel: debounced.cancel, flush: debounced.flush, }; } export default useDebounceFn;
思路:
- 用
useLatest
保证每次都拿到最新的 fn - 用 lodash 的 debounce 包一层,内部调用函数时是执行
fnRef.current(...args)
- 然后返回被包装后的函数 debounced,外部调用时会调用它
- 然后在组件卸载时会取消 debounce
useThrottleFn
useThrottleFn
是用来处理函数节流的 Hook。
主要用到的是 lodash 的 throttle 方法
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {const fnRef = useLatest(fn);const wait = options?.wait ?? 1000;const throttled = useMemo(() =>// 最终都是调用了 lodash 的节流函数throttle((...args: Parameters<T>): ReturnType<T> => {return fnRef.current(...args);},wait,options,),[],);useUnmount(() => {throttled.cancel();});return {run: throttled,// 取消cancel: throttled.cancel,// 立即调用flush: throttled.flush,};}function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) { const fnRef = useLatest(fn); const wait = options?.wait ?? 1000; const throttled = useMemo( () => // 最终都是调用了 lodash 的节流函数 throttle( (...args: Parameters<T>): ReturnType<T> => { return fnRef.current(...args); }, wait, options, ), [], ); useUnmount(() => { throttled.cancel(); }); return { run: throttled, // 取消 cancel: throttled.cancel, // 立即调用 flush: throttled.flush, }; }function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) { const fnRef = useLatest(fn); const wait = options?.wait ?? 1000; const throttled = useMemo( () => // 最终都是调用了 lodash 的节流函数 throttle( (...args: Parameters<T>): ReturnType<T> => { return fnRef.current(...args); }, wait, options, ), [], ); useUnmount(() => { throttled.cancel(); }); return { run: throttled, // 取消 cancel: throttled.cancel, // 立即调用 flush: throttled.flush, }; }
它的思路和 useDebounceFn
一样
useDeepCompareEffect、useDeepCompareLayoutEffect
useDeepCompareEffect
、useDeepCompareLayoutEffect
用法与 useEffect、useLayoutEffect 一致,只不过在依赖发生改变时,使用的是 lodash.isEqual 方法进行深比较
// 入口函数都是 createDeepCompareEffect,只不过一个接受 useEffect 作为参数// 一个接受 useLayoutEffect 作为参数export default createDeepCompareEffect(useEffect);// 源代码export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => {const ref = useRef<DependencyList>();const signalRef = useRef<number>(0);if (deps === undefined || !depsEqual(deps, ref.current)) {ref.current = deps;signalRef.current += 1;}// hook 就是 useEffect / useLayoutEffect// effect 就是传入的 callback 回调hook(effect, [signalRef.current]);};// 入口函数都是 createDeepCompareEffect,只不过一个接受 useEffect 作为参数 // 一个接受 useLayoutEffect 作为参数 export default createDeepCompareEffect(useEffect); // 源代码 export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => { const ref = useRef<DependencyList>(); const signalRef = useRef<number>(0); if (deps === undefined || !depsEqual(deps, ref.current)) { ref.current = deps; signalRef.current += 1; } // hook 就是 useEffect / useLayoutEffect // effect 就是传入的 callback 回调 hook(effect, [signalRef.current]); };// 入口函数都是 createDeepCompareEffect,只不过一个接受 useEffect 作为参数 // 一个接受 useLayoutEffect 作为参数 export default createDeepCompareEffect(useEffect); // 源代码 export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => { const ref = useRef<DependencyList>(); const signalRef = useRef<number>(0); if (deps === undefined || !depsEqual(deps, ref.current)) { ref.current = deps; signalRef.current += 1; } // hook 就是 useEffect / useLayoutEffect // effect 就是传入的 callback 回调 hook(effect, [signalRef.current]); };
思路:
- 通过
ref
记录上一次的依赖项 - 然后判断如果本次依赖是
undefined
或者 两次依赖不同发生了改变,就记录本次依赖ref.current = deps
, - 然后更新
signalRef.current
,从而触发hook
中的回调
这两个 hook 的思路是一样的,上面拿的是 useDeepCompareEffect 来做分析
useInterval 和 useTimeout
他俩源码一样,只不过一个用的 setInterval,一个用的 setTimeout
这里拿 useInterval
分析:
const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {const timerCallback = useMemoizedFn(fn);const timerRef = useRef<NodeJS.Timer | null>(null);//清除函数const clear = useCallback(() => {if (timerRef.current) {clearInterval(timerRef.current);}}, []);useEffect(() => {if (!isNumber(delay) || delay < 0) {return;}if (options.immediate) {//立即执行timerCallback();}// 计时器timerRef.current = setInterval(timerCallback, delay);// 下一次渲染时,清除上一次定时器return clear;}, [delay, options.immediate]);return clear;};export default useInterval;const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => { const timerCallback = useMemoizedFn(fn); const timerRef = useRef<NodeJS.Timer | null>(null); //清除函数 const clear = useCallback(() => { if (timerRef.current) { clearInterval(timerRef.current); } }, []); useEffect(() => { if (!isNumber(delay) || delay < 0) { return; } if (options.immediate) { //立即执行 timerCallback(); } // 计时器 timerRef.current = setInterval(timerCallback, delay); // 下一次渲染时,清除上一次定时器 return clear; }, [delay, options.immediate]); return clear; }; export default useInterval;const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => { const timerCallback = useMemoizedFn(fn); const timerRef = useRef<NodeJS.Timer | null>(null); //清除函数 const clear = useCallback(() => { if (timerRef.current) { clearInterval(timerRef.current); } }, []); useEffect(() => { if (!isNumber(delay) || delay < 0) { return; } if (options.immediate) { //立即执行 timerCallback(); } // 计时器 timerRef.current = setInterval(timerCallback, delay); // 下一次渲染时,清除上一次定时器 return clear; }, [delay, options.immediate]); return clear; }; export default useInterval;
思路:
- 首先检验
delay
,不通过直接return
- 然后判断是否立即执行,如果立即执行则就调用
timerCallback
- 不管是不是立即执行,都开起定时器
- 然后利用 useEffect 的
cleanup
函数机制,在下一次渲染时清除上一次的定时器
跟 setInterval 的区别如下:
- 可以支持第三个参数,通过 immediate 能够立即执行我们的定时器。
- 在变更 delay 的时候,会自动清除旧的定时器,并同时启动新的定时器。
- 通过 useEffect 的返回清除机制,开发者不需要关注清除定时器的逻辑,避免内存泄露问题。这点是很多开发者会忽略的点。
useTimeout 思路跟它一样
useLockFn
useLockFn
用于给一个异步函数增加竞态锁,防止并发执行
function useLockFn<P extends any[] = any[], V extends any = 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);lockRef.current = false;return ret;} catch (e) {lockRef.current = false;throw e;}},[fn],);}export default useLockFn;function useLockFn<P extends any[] = any[], V extends any = 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); lockRef.current = false; return ret; } catch (e) { lockRef.current = false; throw e; } }, [fn], ); } export default useLockFn;function useLockFn<P extends any[] = any[], V extends any = 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); lockRef.current = false; return ret; } catch (e) { lockRef.current = false; throw e; } }, [fn], ); } export default useLockFn;
思路:
- 内部通过
lockRef
来做一个竞态锁的标识,初始化为false
- 当执行传入的
fn
时请求时,lockRef.current
设置为true
- 当重复请求时,此时竞态锁标识为
true
,直接 return,不执行原函数,从而达到加锁的目的
结语
从这几期的源码学习下来,我们发现对于传入的参数,一些会通过 useRef
、useLatest
存下来,然后当某些逻辑的执行需要条件判断时,基本会把判断条件用 useRef
初始化,所以以后自定义 hook 的时候可以参照这样的方式来
以上内容如有错误,欢迎留言指出,一起进步?,也欢迎大家一起讨论。