useUpdateEffect:忽略首次执行,只在依赖更新的时候执行

使用上等同 useEffect

useUpdateLayoutEffect:忽略首次执行,只在依赖更新的时候执行

使用上等同 useEffect
useUpdateEffect 和 useUpdateLayoutEffect 可以一起阅读,因为同时都使用 createUpdateEffect 来创建 hooks:

  1. // useUpdateEffect:传递了 useEffect 来创建 hooks
  2. export default createUpdateEffect(useEffect);
  3. // useUpdateLayoutEffect:传递了 useLayoutEffect 来创建 hooks
  4. 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);

  1. // for react-refresh
  2. hook(() => {
  3. return () => {
  4. isMounted.current = false;
  5. };
  6. }, []);
  7. hook(() => {
  8. if (!isMounted.current) {
  9. isMounted.current = true;
  10. } else {
  11. return effect();
  12. }
  13. }, deps);

};

  1. <a name="v37kW"></a>
  2. ## useAsyncEffect:useEffect 的异步函数版本
  3. - effect 接收 generator 或者异步函数
  4. - 关于 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*))
  5. - yield 关键字后面的表达式值可以返回给生成器调用者
  6. - yield 关键字实际返回了一个迭代器对象,有两个属性 value 和 done,代表【返回值】和【是否完成】
  7. - 调用者使用 next() 配合使用,next() 可以无限调用
  8. - yield 表达式本身没有返回值,后面不接表达式值的话,即返回 undefined
  9. - 关于 yield 相关更深入的例子:[https://www.jianshu.com/p/36c74e4ca9eb](https://www.jianshu.com/p/36c74e4ca9eb)
  10. - [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)
  11. - 更多 Generator 总结在:[https://www.yuque.com/simonezhou/kb/wibfad](https://www.yuque.com/simonezhou/kb/wibfad)
  12. ```typescript
  13. import type { DependencyList } from 'react';
  14. import { useEffect } from 'react';
  15. function useAsyncEffect(
  16. effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  17. deps: DependencyList,
  18. ) {
  19. // 检查是不是 generator
  20. function isGenerator(
  21. val: AsyncGenerator<void, void, void> | Promise<void>,
  22. ): val is AsyncGenerator<void, void, void> {
  23. return typeof val[Symbol.asyncIterator] === 'function';
  24. }
  25. useEffect(() => {
  26. const e = effect();
  27. let cancelled = false;
  28. async function execute() {
  29. if (isGenerator(e)) {
  30. // 这里是个死循环,只要当前组件没销毁,并且生成器还未 done
  31. // 就可以一直执行(await e.next())
  32. while (true) {
  33. const result = await e.next();
  34. if (cancelled || result.done) {
  35. break;
  36. }
  37. }
  38. } else {
  39. // 这里是异步的情况
  40. await e;
  41. }
  42. }
  43. execute();
  44. return () => {
  45. cancelled = true;
  46. };
  47. }, deps);
  48. }

源码可以分开几段阅读:

  • 核心部分 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; }

  1. - 再来看 cancenled 变量是怎么管理的,就是 useEffect return 回调函数里面,将 cancelled 标记成 true 了,说明当组件销毁的时候,会标记成需要取消
  2. ```typescript
  3. function useAsyncEffect(
  4. effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  5. deps: DependencyList,
  6. ) {
  7. ...
  8. useEffect(() => {
  9. const e = effect();
  10. let cancelled = false;
  11. ...
  12. return () => {
  13. cancelled = true;
  14. };
  15. }, deps);
  16. }

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 { return new Promise((resolve) => { setTimeout(() => { resolve(val.length > 0); }, 2000); }); }

const Test = () => { const [value, setValue] = useState(“”); const [pass, setPass] = useState(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 (

{ setValue(e.target.value); }} />

{pass === null && “Checking…”} {pass === false && “Check failed.”} {pass === true && “Check passed.”}

); };

export default () => { const [show, setShow] = useState(true); return ( <> {show && } </> ); };

  1. - 测试可以看到,2s 内如果将组件隐藏,yield 后的语句是不会执行的
  2. ![截屏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)
  3. <a name="NH9BR"></a>
  4. ## useDebounceEffect:为 useEffect 增加防抖
  5. - 依赖 useDebounceFn,返回一个防抖函数
  6. - 依赖 useUpdateEffect,当某个 deps 变化的时候才执行,忽略第一次加载执行
  7. - 整体思路就是使用一个 flag 标记,针对 flag set 值进行防抖,当传入的 deps 更新变动的时候,触发 flag 更新的防抖函数。因为 flag 的更新是防抖过的,所以使用 useUpdateEffect 监听 flag 的变化,当 flag 值变化的时候,才触发传入进来的 effect 回调函数
  8. ```typescript
  9. import { useEffect, useState } from 'react';
  10. import type { DependencyList, EffectCallback } from 'react';
  11. import type { DebounceOptions } from '../useDebounce/debounceOptions';
  12. import useDebounceFn from '../useDebounceFn';
  13. import useUnmount from '../useUnmount';
  14. import useUpdateEffect from '../useUpdateEffect';
  15. function useDebounceEffect(
  16. effect: EffectCallback,
  17. deps?: DependencyList,
  18. options?: DebounceOptions,
  19. ) {
  20. const [flag, setFlag] = useState({});
  21. // 触发 flag 更新的防抖函数
  22. const { run, cancel } = useDebounceFn(() => {
  23. setFlag({});
  24. }, options);
  25. // deps 依赖变化的时候,触发 flag 的防抖更新函数
  26. useEffect(() => {
  27. return run();
  28. }, deps);
  29. useUnmount(cancel);
  30. // flag 的更新是防抖过的,所以 effect 的触发也是
  31. useUpdateEffect(effect, [flag]);
  32. }

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 才会命中右值

    1. 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 为立即执行该函数

    1. interface DebouncedFunc<T extends (...args: any[]) => any> {
    2. /**
    3. * Call the original function, but applying the debounce rules.
    4. *
    5. * If the debounced function can be run immediately, this calls it and returns its return
    6. * value.
    7. *
    8. * Otherwise, it returns the return value of the last invocation, or undefined if the debounced
    9. * function was not invoked yet.
    10. */
    11. (...args: Parameters<T>): ReturnType<T> | undefined;
    12. /**
    13. * Throw away any pending invocation of the debounced function.
    14. */
    15. cancel(): void;
    16. /**
    17. * If there is a pending invocation of the debounced function, invoke it immediately and return
    18. * its return value.
    19. *
    20. * Otherwise, return the value from the last invocation, or undefined if the debounced function
    21. * was never invoked.
    22. */
    23. flush(): ReturnType<T> | undefined;
    24. }
  • 有了以上前提,可以看到该 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 useDebounceFn(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( ((…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, }; }

  1. <a name="q6m5v"></a>
  2. ## useThrottleFn:处理函数的节流
  3. 是 useThrottle 与 useThrottleEffect 的基底 hooks,整体流程大致与 debounce 是一样的
  4. - 使用的是 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 源码)
  5. - 传入 fn 回调函数,经过 lodash throttle 包装后,返回节流后的函数,lodash throttle 返回的节流函数还另外附赠了两个功能:cancel 为取消该节流函数,flush 为立即执行该函数
  6. - throttle 返回的是 DebouncedFunc<T> 类型,与 debounce 返回值类型相同
  7. ```typescript
  8. 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 useThrottleFn(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( ((…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, }; }

  1. <a name="AtyjR"></a>
  2. ## useThrottleEffect:为 useEffect 增加节流
  3. - 依赖 useThrottleFn,返回一个防抖函数
  4. - 依赖 useUpdateEffect,当某个 deps 变化的时候才执行,忽略第一次加载执行
  5. - 整体思路就是使用一个 flag 标记,针对 flag set 值进行节流,当传入的 deps 更新变动的时候,触发 flag 更新的节流函数。因为 flag 的更新是节流过的,所以使用 useUpdateEffect 监听 flag 的变化,当 flag 值变化的时候,才触发传入进来的 effect 回调函数
  6. ```typescript
  7. import { useEffect, useState } from 'react';
  8. import type { DependencyList, EffectCallback } from 'react';
  9. import type { ThrottleOptions } from '../useThrottle/throttleOptions';
  10. import useThrottleFn from '../useThrottleFn';
  11. import useUnmount from '../useUnmount';
  12. import useUpdateEffect from '../useUpdateEffect';
  13. function useThrottleEffect(
  14. effect: EffectCallback,
  15. deps?: DependencyList,
  16. options?: ThrottleOptions,
  17. ) {
  18. const [flag, setFlag] = useState({});
  19. // 触发 flag 更新的节流函数
  20. const { run, cancel } = useThrottleFn(() => {
  21. setFlag({});
  22. }, options);
  23. // 当传入的 deps 更新时,触发 flag 更新节流函数
  24. useEffect(() => {
  25. return run();
  26. }, deps);
  27. useUnmount(cancel);
  28. // 当 flag 更新时,触发 effect 回调函数,flag 的更新是节流过的
  29. useUpdateEffect(effect, [flag]);
  30. }

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(); const signalRef = useRef(0);

if (!depsEqual(deps, ref.current)) { ref.current = deps; signalRef.current += 1; }

useEffect(effect, [signalRef.current]); };

  1. <a name="Z5bhl"></a>
  2. ## useInterval:setInterval
  3. 针对 setInterval 的封装,动态修改 delay 值可以设置时间间隔变化,或者取消计时器
  4. ```typescript
  5. import { useEffect } from 'react';
  6. import useLatest from '../useLatest';
  7. function useInterval(
  8. fn: () => void,
  9. delay: number | undefined,
  10. options?: {
  11. immediate?: boolean;
  12. },
  13. ) {
  14. const immediate = options?.immediate;
  15. const fnRef = useLatest(fn);
  16. useEffect(() => {
  17. // 如果是 false 或者 undefined 则取消计时器
  18. if (typeof delay !== 'number' || delay <= 0) return;
  19. // 是否在注册时立即执行
  20. if (immediate) {
  21. fnRef.current();
  22. }
  23. // 计时器
  24. const timer = setInterval(() => {
  25. fnRef.current();
  26. }, delay);
  27. // 注销时,取消计时器
  28. return () => {
  29. clearInterval(timer);
  30. };
  31. }, [delay]); // 监听 delay 值的变化
  32. }

useTimeout:setTimeout

针对 setInterval 的封装,动态修改 delay 值可以设置时间间隔变化,或者取消 setTimeout

  1. import { useEffect } from 'react';
  2. import useLatest from '../useLatest';
  3. function useTimeout(fn: () => void, delay: number | undefined): void {
  4. const fnRef = useLatest(fn);
  5. useEffect(() => {
  6. // 取消
  7. if (typeof delay !== 'number' || delay <= 0) return;
  8. // 计时器
  9. const timer = setTimeout(() => {
  10. fnRef.current();
  11. }, delay);
  12. // 注销取消
  13. return () => {
  14. clearTimeout(timer);
  15. };
  16. }, [delay]); // 监听 delay 变化
  17. }

useLockFn:给异步函数增加竞态锁,防止并发执行

思路也比较简单,使用 lockRef 来记录是否在执行

  1. import { useRef, useCallback } from 'react';
  2. function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) {
  3. const lockRef = useRef(false);
  4. return useCallback(
  5. async (...args: P) => {
  6. // 如果在执行,直接 return
  7. if (lockRef.current) return;
  8. lockRef.current = true; // 执行前上锁
  9. try {
  10. const ret = await fn(...args);
  11. lockRef.current = false; // 执行完解锁
  12. return ret;
  13. } catch (e) {
  14. lockRef.current = false; // 执行完解锁
  15. throw e;
  16. }
  17. },
  18. [fn],
  19. );
  20. }

useUpdate:强制刷新组件重新渲染

设置了个空的 state,每次都强行设置 state,就能触发更新

  1. const useUpdate = () => {
  2. const [, setState] = useState({});
  3. return useCallback(() => setState({}), []);
  4. };