useUpdateEffect:忽略首次执行,只在依赖更新的时候执行
使用上等同 useEffect
useUpdateLayoutEffect:忽略首次执行,只在依赖更新的时候执行
使用上等同 useEffect
useUpdateEffect 和 useUpdateLayoutEffect 可以一起阅读,因为同时都使用 createUpdateEffect 来创建 hooks:
// useUpdateEffect:传递了 useEffect 来创建 hooks
export default createUpdateEffect(useEffect);
// useUpdateLayoutEffect:传递了 useLayoutEffect 来创建 hooks
export default createUpdateEffect(useLayoutEffect);
createUpdateEffect 部分:
- 使用 isMounted 来标记是不是初始化
- hook(() => {}, []) 为第一次加载,设置 isMounted 为 false
- hook(() => {}, [deps]) 时,首先判断 isMounted 是不是 false,如果是则设置 isMounted 为 true,再下一次更新时,isMounted 已经是 true 了,那么判断到 isMounted 是 true 的时候,则执行 effect() 回调函数
import { useRef } from 'react';
import type { useEffect, useLayoutEffect } from 'react';
type effectHookType = typeof useEffect | typeof useLayoutEffect;
export const createUpdateEffect: (hook: effectHookType) => effectHookType =
(hook) => (effect, deps) => {
const isMounted = useRef(false);
// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
useAsyncEffect:useEffect 的异步函数版本
- effect 接收 generator 或者异步函数
- 关于 generator 生成器函数(developer.mozilla.org/zh-CN/docs/…)
- yield 关键字后面的表达式值可以返回给生成器调用者
- yield 关键字实际返回了一个迭代器对象,有两个属性 value 和 done,代表【返回值】和【是否完成】
- 调用者使用 next() 配合使用,next() 可以无限调用
- yield 表达式本身没有返回值,后面不接表达式值的话,即返回 undefined
- 关于 yield 相关更深入的例子:www.jianshu.com/p/36c74e4ca…
- developer.mozilla.org/zh-CN/docs/…
- 更多 Generator 总结在:www.yuque.com/simonezhou/…
import type { DependencyList } from 'react';
import { useEffect } from 'react';
function useAsyncEffect(
effect: () => AsyncGenerator<void, void, void> | Promise<void>,
deps: DependencyList,
) {
// 检查是不是 generator
function isGenerator(
val: AsyncGenerator<void, void, void> | Promise<void>,
): val is AsyncGenerator<void, void, void> {
return typeof val[Symbol.asyncIterator] === 'function';
}
useEffect(() => {
const e = effect();
let cancelled = false;
async function execute() {
if (isGenerator(e)) {
// 这里是个死循环,只要当前组件没销毁,并且生成器还未 done
// 就可以一直执行(await e.next())
while (true) {
const result = await e.next();
if (cancelled || result.done) {
break;
}
}
} else {
// 这里是异步的情况
await e;
}
}
execute();
return () => {
cancelled = true;
};
}, deps);
}
源码可以分开几段阅读:
- 核心部分 execute()
// 1.execute 是个异步函数,然后在外层调用
async function execute() {
...
}
execute();
// 2. 内部首先是个 if else 判断
// 其实就是判断是不是生成器,如果是生成器的话就走一个逻辑,是异步就走另一个逻辑
if (isGenerator(e)) { ... } else { ... }
// 3. 先来看【不是】生成器的情况,简单粗暴的 await 调用就好了
await e
// 4. 再来看【是】生成器的情况,是个 while 死循环
while (true) {
const result = await e.next();
if (cancelled || result.done) {
break;
}
}
// 把里面的 if 去掉的话,其实就是不停的去执行 next()
while (true) {
const result = await e.next();
...
}
// 再来看下死循环退出的条件:
// 要不就是 cancelled 的时候退出
// 要不就是 next() 执行到返回 done 的时候推出,即生成器函数一步一步执行结束了
if (cancelled || result.done) { break; }
- 再来看 cancenled 变量是怎么管理的,就是 useEffect return 回调函数里面,将 cancelled 标记成 true 了,说明当组件销毁的时候,会标记成需要取消
function useAsyncEffect(
effect: () => AsyncGenerator<void, void, void> | Promise<void>,
deps: DependencyList,
) {
...
useEffect(() => {
const e = effect();
let cancelled = false;
...
return () => {
cancelled = true;
};
}, deps);
}
ahooks 关于 generator 的使用,给的 demo 例子描述得不太直观清晰,在原来的 demo 修改了下可以看出效果:
- 在 input 上输入值,模拟异步检查正确性,2s 后返回输入值的正确性(length > 0 则为 true,否则为 false),在 2s 过程内,点击按钮将组件隐藏(即触发 unmounted),在 yield 语句之后 console.log,查看是否依然输出
import React, { useState } from "react";
import { useAsyncEffect } from "ahooks";
function mockCheck(val: string): Promise<boolean> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(val.length > 0);
}, 2000);
});
}
const Test = () => {
const [value, setValue] = useState("");
const [pass, setPass] = useState<boolean>(null);
useAsyncEffect(
async function* () {
setPass(null);
const result = await mockCheck(value);
yield; // Check whether the effect is still valid, if it is has been cleaned up, stop at here.
console.log('?????')
setPass(result);
},
[value]
);
return (
<div>
<input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
<p>
{pass === null && "Checking..."}
{pass === false && "Check failed."}
{pass === true && "Check passed."}
</p>
</div>
);
};
export default () => {
const [show, setShow] = useState<boolean>(true);
return (
<>
<button onClick={() => setShow(false)}>unmounted it</button>
{show && <Test />}
</>
);
};
- 测试可以看到,2s 内如果将组件隐藏,yield 后的语句是不会执行的
useDebounceEffect:为 useEffect 增加防抖
- 依赖 useDebounceFn,返回一个防抖函数
- 依赖 useUpdateEffect,当某个 deps 变化的时候才执行,忽略第一次加载执行
- 整体思路就是使用一个 flag 标记,针对 flag set 值进行防抖,当传入的 deps 更新变动的时候,触发 flag 更新的防抖函数。因为 flag 的更新是防抖过的,所以使用 useUpdateEffect 监听 flag 的变化,当 flag 值变化的时候,才触发传入进来的 effect 回调函数
import { useEffect, useState } from 'react';
import type { DependencyList, EffectCallback } from 'react';
import type { DebounceOptions } from '../useDebounce/debounceOptions';
import useDebounceFn from '../useDebounceFn';
import useUnmount from '../useUnmount';
import useUpdateEffect from '../useUpdateEffect';
function useDebounceEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: DebounceOptions,
) {
const [flag, setFlag] = useState({});
// 触发 flag 更新的防抖函数
const { run, cancel } = useDebounceFn(() => {
setFlag({});
}, options);
// deps 依赖变化的时候,触发 flag 的防抖更新函数
useEffect(() => {
return run();
}, deps);
useUnmount(cancel);
// flag 的更新是防抖过的,所以 effect 的触发也是
useUpdateEffect(effect, [flag]);
}
useDebounceFn:处理函数的防抖
是 useDebounceEffect 与 useDebounce 的基底 hooks
意外的惊喜,对于 wait 参数的赋值,使用了 ?? 运算符 developer.mozilla.org/en-US/docs/…:
- || 运算符,当为 null / undefined / 0 / false / '' 时都会命中右值
- ?? 运算符,只有当 null 或者 undefined 才会命中右值
const wait = options?.wait ?? 1000;
进入正题:
- 使用的是 lodash 的 debounce:lodash.com/docs/4.17.1…,options 参数实则 lodash debounce 的 options 参数,wait 参数也是 lodash 支持的参数(暂不深究 lodash debounce 源码)
- 传入 fn 回调函数,经过 lodash debounce 包装后,返回防抖后的函数,lodash debounce 返回的防抖函数还另外附赠了两个功能:cancel 为取消该防抖函数,flush 为立即执行该函数
interface DebouncedFunc<T extends (...args: any[]) => any> {
/**
* Call the original function, but applying the debounce rules.
*
* If the debounced function can be run immediately, this calls it and returns its return
* value.
*
* Otherwise, it returns the return value of the last invocation, or undefined if the debounced
* function was not invoked yet.
*/
(...args: Parameters<T>): ReturnType<T> | undefined;
/**
* Throw away any pending invocation of the debounced function.
*/
cancel(): void;
/**
* If there is a pending invocation of the debounced function, invoke it immediately and return
* its return value.
*
* Otherwise, return the value from the last invocation, or undefined if the debounced function
* was never invoked.
*/
flush(): ReturnType<T> | undefined;
}
- 有了以上前提,可以看到该 hooks 返回了几个基础操作,run 为 经过 lodash debounce 包装后的防抖函数,同时再把该防抖函数的 cancel 与 flush 函数暴露出来。同时增加钩子 useUnmount 在注销时,取消该函数防抖
import debounce from 'lodash/debounce';
import { useMemo } from 'react';
import type { DebounceOptions } from '../useDebounce/debounceOptions';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
type noop = (...args: any) => any;
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
if (process.env.NODE_ENV === 'development') {
if (typeof fn !== 'function') {
console.error(`useDebounceFn expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const debounced = useMemo(
() =>
debounce<T>(
((...args: any[]) => {
return fnRef.current(...args);
}) as T,
wait,
options,
),
[],
);
useUnmount(() => {
debounced.cancel();
});
return {
run: debounced as unknown as T,
cancel: debounced.cancel,
flush: debounced.flush,
};
}
useThrottleFn:处理函数的节流
是 useThrottle 与 useThrottleEffect 的基底 hooks,整体流程大致与 debounce 是一样的
- 使用的是 lodash 的 throttle lodash.com/docs/4.17.1…,options 参数实则 lodash throttle 的 options 参数,wait 参数也是 lodash 支持的参数(暂不深究 lodash throttle 源码)
- 传入 fn 回调函数,经过 lodash throttle 包装后,返回节流后的函数,lodash throttle 返回的节流函数还另外附赠了两个功能:cancel 为取消该节流函数,flush 为立即执行该函数
- throttle 返回的是 DebouncedFunc 类型,与 debounce 返回值类型相同
throttle<T extends (...args: any) => any>(func: T, wait?: number, options?: ThrottleSettings): DebouncedFunc<T>;
- 有了以上前提,可以看到该 hooks 返回了几个基础操作,run 为 经过 lodash throttle 包装后的节流函数,同时再把该节流函数的 cancel 与 flush 函数暴露出来。同时增加钩子 useUnmount 在注销时,取消该函数节流
import throttle from 'lodash/throttle';
import { useMemo } from 'react';
import useLatest from '../useLatest';
import type { ThrottleOptions } from '../useThrottle/throttleOptions';
import useUnmount from '../useUnmount';
type noop = (...args: any) => any;
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
if (process.env.NODE_ENV === 'development') {
if (typeof fn !== 'function') {
console.error(`useThrottleFn expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const throttled = useMemo(
() =>
throttle<T>(
((...args: any[]) => {
return fnRef.current(...args);
}) as T,
wait,
options,
),
[],
);
useUnmount(() => {
throttled.cancel();
});
return {
run: throttled as unknown as T,
cancel: throttled.cancel,
flush: throttled.flush,
};
}
useThrottleEffect:为 useEffect 增加节流
- 依赖 useThrottleFn,返回一个防抖函数
- 依赖 useUpdateEffect,当某个 deps 变化的时候才执行,忽略第一次加载执行
- 整体思路就是使用一个 flag 标记,针对 flag set 值进行节流,当传入的 deps 更新变动的时候,触发 flag 更新的节流函数。因为 flag 的更新是节流过的,所以使用 useUpdateEffect 监听 flag 的变化,当 flag 值变化的时候,才触发传入进来的 effect 回调函数
import { useEffect, useState } from 'react';
import type { DependencyList, EffectCallback } from 'react';
import type { ThrottleOptions } from '../useThrottle/throttleOptions';
import useThrottleFn from '../useThrottleFn';
import useUnmount from '../useUnmount';
import useUpdateEffect from '../useUpdateEffect';
function useThrottleEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: ThrottleOptions,
) {
const [flag, setFlag] = useState({});
// 触发 flag 更新的节流函数
const { run, cancel } = useThrottleFn(() => {
setFlag({});
}, options);
// 当传入的 deps 更新时,触发 flag 更新节流函数
useEffect(() => {
return run();
}, deps);
useUnmount(cancel);
// 当 flag 更新时,触发 effect 回调函数,flag 的更新是节流过的
useUpdateEffect(effect, [flag]);
}
useDeepCompareEffect:deps 比较不一致,才触发的 useEffect
- 使用的是 lodash isEqual 进行判断:lodash.com/docs/4.17.1…
- 整个 deps 列表不一样,才会触发 effect 回调函数
- 使用 signalRef 标记是否更新,如果有的话则 +1
- useEffect 监听 signalRef.current 的变化,如果变化了,才触发 effect
import isEqual from 'lodash/isEqual';
import { useEffect, useRef } from 'react';
import type { DependencyList, EffectCallback } from 'react';
const depsEqual = (aDeps: DependencyList, bDeps: DependencyList = []) => {
return isEqual(aDeps, bDeps);
};
const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
const ref = useRef<DependencyList>();
const signalRef = useRef<number>(0);
if (!depsEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}
useEffect(effect, [signalRef.current]);
};
useInterval:setInterval
针对 setInterval 的封装,动态修改 delay 值可以设置时间间隔变化,或者取消计时器
import { useEffect } from 'react';
import useLatest from '../useLatest';
function useInterval(
fn: () => void,
delay: number | undefined,
options?: {
immediate?: boolean;
},
) {
const immediate = options?.immediate;
const fnRef = useLatest(fn);
useEffect(() => {
// 如果是 false 或者 undefined 则取消计时器
if (typeof delay !== 'number' || delay <= 0) return;
// 是否在注册时立即执行
if (immediate) {
fnRef.current();
}
// 计时器
const timer = setInterval(() => {
fnRef.current();
}, delay);
// 注销时,取消计时器
return () => {
clearInterval(timer);
};
}, [delay]); // 监听 delay 值的变化
}
useTimeout:setTimeout
针对 setInterval 的封装,动态修改 delay 值可以设置时间间隔变化,或者取消 setTimeout
import { useEffect } from 'react';
import useLatest from '../useLatest';
function useTimeout(fn: () => void, delay: number | undefined): void {
const fnRef = useLatest(fn);
useEffect(() => {
// 取消
if (typeof delay !== 'number' || delay <= 0) return;
// 计时器
const timer = setTimeout(() => {
fnRef.current();
}, delay);
// 注销取消
return () => {
clearTimeout(timer);
};
}, [delay]); // 监听 delay 变化
}
useLockFn:给异步函数增加竞态锁,防止并发执行
思路也比较简单,使用 lockRef 来记录是否在执行
import { useRef, useCallback } from 'react';
function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) {
const lockRef = useRef(false);
return useCallback(
async (...args: P) => {
// 如果在执行,直接 return
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],
);
}
useUpdate:强制刷新组件重新渲染
设置了个空的 state,每次都强行设置 state,就能触发更新
const useUpdate = () => {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
};