useUpdateEffect:忽略首次执行,只在依赖更新的时候执行
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() 回调函数 ```javascript 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);
};
<a name="v37kW"></a>
## useAsyncEffect:useEffect 的异步函数版本
- effect 接收 generator 或者异步函数
- 关于 generator 生成器函数([https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function*](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function*))
- yield 关键字后面的表达式值可以返回给生成器调用者
- yield 关键字实际返回了一个迭代器对象,有两个属性 value 和 done,代表【返回值】和【是否完成】
- 调用者使用 next() 配合使用,next() 可以无限调用
- yield 表达式本身没有返回值,后面不接表达式值的话,即返回 undefined
- 关于 yield 相关更深入的例子:[https://www.jianshu.com/p/36c74e4ca9eb](https://www.jianshu.com/p/36c74e4ca9eb)
- [https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator)
- 更多 Generator 总结在:[https://www.yuque.com/simonezhou/kb/wibfad](https://www.yuque.com/simonezhou/kb/wibfad)
```typescript
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() ```typescript // 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 了,说明当组件销毁的时候,会标记成需要取消
```typescript
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,查看是否依然输出 ```typescript import React, { useState } from “react”; import { useAsyncEffect } from “ahooks”;
function mockCheck(val: string): Promise
const Test = () => {
const [value, setValue] = useState(“”);
const [pass, setPass] = useState
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 (
{pass === null && “Checking…”} {pass === false && “Check failed.”} {pass === true && “Check passed.”}
export default () => {
const [show, setShow] = useState
- 测试可以看到,2s 内如果将组件隐藏,yield 后的语句是不会执行的
![截屏2022-01-07 下午3.38.20.png](https://cdn.nlark.com/yuque/0/2022/png/22895623/1641541119002-6f3053e6-9899-4db2-87c7-d3250a1d12de.png#clientId=u6ffa58d2-d9c1-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=720&id=u342c0828&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-01-07%20%E4%B8%8B%E5%8D%883.38.20.png&originHeight=1440&originWidth=2774&originalType=binary&ratio=1&rotation=0&showTitle=false&size=664664&status=done&style=none&taskId=u6a3b208c-720d-4dad-8735-922a0dc8785&title=&width=1387)
<a name="NH9BR"></a>
## useDebounceEffect:为 useEffect 增加防抖
- 依赖 useDebounceFn,返回一个防抖函数
- 依赖 useUpdateEffect,当某个 deps 变化的时候才执行,忽略第一次加载执行
- 整体思路就是使用一个 flag 标记,针对 flag set 值进行防抖,当传入的 deps 更新变动的时候,触发 flag 更新的防抖函数。因为 flag 的更新是防抖过的,所以使用 useUpdateEffect 监听 flag 的变化,当 flag 值变化的时候,才触发传入进来的 effect 回调函数
```typescript
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 参数的赋值,使用了 ?? 运算符 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator:
- || 运算符,当为 null / undefined / 0 / false / ‘’ 时都会命中右值
?? 运算符,只有当 null 或者 undefined 才会命中右值
const wait = options?.wait ?? 1000;
进入正题:
使用的是 lodash 的 debounce:https://lodash.com/docs/4.17.15#debounce,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 在注销时,取消该函数防抖 ```javascript 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 useDebounceFnuseDebounceFn expected parameter is a function, got ${typeof fn}
);
}
}
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const debounced = useMemo(
() =>
debounce
useUnmount(() => { debounced.cancel(); });
return { run: debounced as unknown as T, cancel: debounced.cancel, flush: debounced.flush, }; }
<a name="q6m5v"></a>
## useThrottleFn:处理函数的节流
是 useThrottle 与 useThrottleEffect 的基底 hooks,整体流程大致与 debounce 是一样的
- 使用的是 lodash 的 throttle [https://lodash.com/docs/4.17.15#throttle](https://lodash.com/docs/4.17.15#throttle),options 参数实则 lodash throttle 的 options 参数,wait 参数也是 lodash 支持的参数(暂不深究 lodash throttle 源码)
- 传入 fn 回调函数,经过 lodash throttle 包装后,返回节流后的函数,lodash throttle 返回的节流函数还另外附赠了两个功能:cancel 为取消该节流函数,flush 为立即执行该函数
- throttle 返回的是 DebouncedFunc<T> 类型,与 debounce 返回值类型相同
```typescript
throttle<T extends (...args: any) => any>(func: T, wait?: number, options?: ThrottleSettings): DebouncedFunc<T>;
- 有了以上前提,可以看到该 hooks 返回了几个基础操作,run 为 经过 lodash throttle 包装后的节流函数,同时再把该节流函数的 cancel 与 flush 函数暴露出来。同时增加钩子 useUnmount 在注销时,取消该函数节流 ```typescript 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 useThrottleFnuseThrottleFn expected parameter is a function, got ${typeof fn}
);
}
}
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const throttled = useMemo(
() =>
throttle
useUnmount(() => { throttled.cancel(); });
return { run: throttled as unknown as T, cancel: throttled.cancel, flush: throttled.flush, }; }
<a name="AtyjR"></a>
## useThrottleEffect:为 useEffect 增加节流
- 依赖 useThrottleFn,返回一个防抖函数
- 依赖 useUpdateEffect,当某个 deps 变化的时候才执行,忽略第一次加载执行
- 整体思路就是使用一个 flag 标记,针对 flag set 值进行节流,当传入的 deps 更新变动的时候,触发 flag 更新的节流函数。因为 flag 的更新是节流过的,所以使用 useUpdateEffect 监听 flag 的变化,当 flag 值变化的时候,才触发传入进来的 effect 回调函数
```typescript
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 进行判断:https://lodash.com/docs/4.17.15#isEqual
- 整个 deps 列表不一样,才会触发 effect 回调函数
- 使用 signalRef 标记是否更新,如果有的话则 +1
- useEffect 监听 signalRef.current 的变化,如果变化了,才触发 effect ```typescript 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
if (!depsEqual(deps, ref.current)) { ref.current = deps; signalRef.current += 1; }
useEffect(effect, [signalRef.current]); };
<a name="Z5bhl"></a>
## useInterval:setInterval
针对 setInterval 的封装,动态修改 delay 值可以设置时间间隔变化,或者取消计时器
```typescript
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({}), []);
};