【解读 ahooks 源码系列】Effect 篇(一)

本文是 ahooks 源码(v3.7.4)系列的第十篇——Effect 篇(一)

往期文章:

本文主要解读 useUpdateEffectuseUpdateLayoutEffectuseAsyncEffectuseDebounceFnuseDebounceEffectuseThrottleFnuseThrottleEffect 的源码实现

useUpdateEffect

useUpdateEffect 用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行。

官方文档

API 与 React.useEffect 完全一致。

基本用法

官方在线 Demo

import React, { useEffect, useState } from 'react';
import { useUpdateEffect } from 'ahooks';

export default () => {
  const [count, setCount] = useState(0);
  const [effectCount, setEffectCount] = useState(0);
  const [updateEffectCount, setUpdateEffectCount] = useState(0);

  useEffect(() => {
    setEffectCount((c) => c + 1);
  }, [count]);

  useUpdateEffect(() => {
    setUpdateEffectCount((c) => c + 1);
    return () => {
      // do something
    };
  }, [count]); // you can include deps array if necessary

  return (
    <div>
      <p>effectCount: {effectCount}</p>
      <p>updateEffectCount: {updateEffectCount}</p>
      <p>
        <button type="button" onClick={() => setCount((c) => c + 1)}>
          reRender
        </button>
      </p>
    </div>
  );
};

实现思路

  • 初始化一个 isMounted 标识,默认为 false;首次 useEffect 执行完后置为 true
  • 后续执行 useEffect 的时候判断 isMounted 标识是否为 true,true 则执行外部传入的 effect 函数;卸载的时候将 isMounted 标识重置为 false

核心实现

里面其实是实现了 createUpdateEffect 这个函数:

export default createUpdateEffect(useEffect);
type EffectHookType = typeof useEffect | typeof useLayoutEffect;

export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
  (hook) => (effect, deps) => {
    const isMounted = useRef(false);

    // for react-refresh
    hook(() => {
      // 卸载时重置 isMounted 为 false
      return () => {
        isMounted.current = false;
      };
    }, []);

    hook(() => {
      if (!isMounted.current) {
        // 首次执行完设置为 true
        isMounted.current = true;
      } else {
        // 第一次后则执行函数
        return effect();
      }
    }, deps);
  };

完整源码

useUpdateLayoutEffect

useUpdateLayoutEffect 用法等同于 useLayoutEffect,但是会忽略首次执行,只在依赖更新时执行。

官方文档

API 与 React.useLayoutEffect 完全一致。

基本用法

官方在线 Demo

使用上与 useLayoutEffect 完全相同,只是它忽略了首次执行,且只在依赖项更新时执行。

import React, { useLayoutEffect, useState } from 'react';
import { useUpdateLayoutEffect } from 'ahooks';

export default () => {
  const [count, setCount] = useState(0);
  const [layoutEffectCount, setLayoutEffectCount] = useState(0);
  const [updateLayoutEffectCount, setUpdateLayoutEffectCount] = useState(0);

  useLayoutEffect(() => {
    setLayoutEffectCount((c) => c + 1);
  }, [count]);

  useUpdateLayoutEffect(() => {
    setUpdateLayoutEffectCount((c) => c + 1);
    return () => {
      // do something
    };
  }, [count]); // you can include deps array if necessary

  return (
    <div>
      <p>layoutEffectCount: {layoutEffectCount}</p>
      <p>updateLayoutEffectCount: {updateLayoutEffectCount}</p>
      <p>
        <button type="button" onClick={() => setCount((c) => c + 1)}>
          reRender
        </button>
      </p>
    </div>
  );
};

核心实现

和 useUpdateEffect 一样,都是调用了 createUpdateEffect 方法,区别只是传入的 hook 是 useLayoutEffect。其余源码同上,就不再列举了

export default createUpdateEffect(useLayoutEffect);

完整源码

useAsyncEffect

useEffect 支持异步函数。

官方文档

基本用法

官方在线 Demo

组件加载时进行异步的检查

import { useAsyncEffect } from 'ahooks';
import React, { useState } from 'react';

function mockCheck(): Promise<boolean> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(true);
    }, 3000);
  });
}

export default () => {
  const [pass, setPass] = useState<boolean>();

  useAsyncEffect(async () => {
    setPass(await mockCheck());
  }, []);

  return (
    <div>
      {pass === undefined && 'Checking...'}
      {pass === true && 'Check passed.'}
    </div>
  );
};

为什么需要该 Hook

在使用 useEffect 进行数据获取的时候,如果使用 async/await 的时候,会看到控制台有警告:

Warning: An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => …) or returned a Promise. Instead, write the async function inside your effect and call it immediately:

useEffect(async () => {
  const data = await fetchData();
}, [fetchData])

第一个参数是函数,它可以不返回内容 (return undefined) 或一个销毁函数。如果返回的是异步函数(Promise),则会导致 React 在调用销毁函数的时候报错。返回值是异步,也难以预知代码的执行结果,可能出现难以定位的 Bug,故返回值不支持异步。

如何让 useEffect 支持使用异步函数

  1. 自执行函数 IIFE
useEffect(async () => {
  (async function getData() {
    const data = await fetchData();
  })()
}, [])
  1. useEffect 里面抽离封装异步函数,再调用
useEffect(() => {
  const getData = async () => {
    const data = await fetchData();
  };
  getData()
}, [])
  1. 外部定义异步函数,useEffect 直接调用
const getData = async () => {
  const data = await fetchData();
};
useEffect(() => {
  getData()
}, [])
  1. 自定义 Hook 实现

useAsyncEffect 就是一种实现了,省略上面那些代码处理

API

function useAsyncEffect(
  effect: () => AsyncGenerator | Promise,
  deps: DependencyList
);

核心实现

  • Symbol.asyncIterator 符号指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于 for await…of 循环。

ahooks 的实现使用了上述的第二种解决方式,不过还增加了 AsyncGenerator 支持

function useAsyncEffect(
  effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  deps?: DependencyList,
) {
  // 判断是否为 AsyncGenerator
  function isAsyncGenerator(
    val: AsyncGenerator<void, void, void> | Promise<void>,
  ): val is AsyncGenerator<void, void, void> {
    return isFunction(val[Symbol.asyncIterator]);
  }

  useEffect(() => {
    // effect 异步函数
    const e = effect();
    let cancelled = false;
    async function execute() {
      // 如果是 Generator 异步函数,则通过 next() 的方式执行
      if (isAsyncGenerator(e)) {
        while (true) {
          const result = await e.next();
          // [Generator 函数执行完] 或 [当前 useEffect 已经被清理]
          if (result.done || cancelled) {
            break;
          }
        }
      } else {
        // Promise 函数
        await e;
      }
    }
    execute(); // 执行异步函数
    return () => {
      // 设置表示当前 useEffect 已执行完销毁操作的标识
      cancelled = true;
    };
  }, deps);
}

看到上面的实现,发现 useAsyncEffect 并没有百分百兼容 useEffect 用法,它的销毁函数是只设置了已清理标识:

return () => {
  cancelled = true;
};

这种做法的观点是认为延迟清除机制是不对的,应该是一种取消机制。否则,在钩子已经被取消之后,回调函数仍然有机会对外部状态产生影响。

具体可以看这个大佬的文章:如何让 useEffect 支持 async…await?

完整源码

useDebounceFn

用来处理防抖函数的 Hook。

官方文档

基本用法

官方在线 Demo

频繁调用 run,但只会在所有点击完成 500ms 后执行一次相关函数

import { useDebounceFn } from 'ahooks';
import React, { useState } from 'react';

export default () => {
  const [value, setValue] = useState(0);
  const { run } = useDebounceFn(
    () => {
      setValue(value + 1);
    },
    {
      wait: 500,
    },
  );

  return (
    <div>
      <p style={{ marginTop: 16 }}> Clicked count: {value} </p>
      <button type="button" onClick={run}>
        Click fast!
      </button>
    </div>
  );
};

核心实现

支持的选项,都是 lodash.debounce 里面的参数:

interface DebounceOptions {
  wait?: number; // 等待时间,单位为毫秒
  leading?: boolean; // 是否在延迟开始前调用函数
  trailing?: boolean; // 是否在延迟开始后调用函数
  maxWait?: number; // 最大等待时间,单位为毫秒
}
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(`useDebounceFn expected parameter is a function, got ${typeof fn}`);
    }
  }

  // 最新的 fn 防抖函数
  const fnRef = useLatest(fn);

  // 默认是 1000 毫秒
  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, // 触发执行 fn
    cancel: debounced.cancel, // 取消当前防抖
    flush: debounced.flush, // 当前防抖立即调用
  };
}

完整源码

useDebounceEffect

为 useEffect 增加防抖的能力。

官方文档

基本用法

官方在线 Demo

import { useDebounceEffect } from 'ahooks';
import React, { useState } from 'react';

export default () => {
  const [value, setValue] = useState('hello');
  const [records, setRecords] = useState<string[]>([]);
  useDebounceEffect(
    () => {
      setRecords((val) => [...val, value]);
    },
    [value],
    {
      wait: 1000,
    },
  );
  return (
    <div>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Typed value"
        style={{ width: 280 }}
      />
      <p style={{ marginTop: 16 }}>
        <ul>
          {records.map((record, index) => (
            <li key={index}>{record}</li>
          ))}
        </ul>
      </p>
    </div>
  );
};

核心实现

它的实现依赖于 useDebounceFn hook。

实现逻辑:本来 deps 更新,effect 函数就立即执行的;现在 deps 更新,执行 防抖函数 setFlag 来更新 flag,而 flag 又被 useUpdateEffect 监听,通过 useUpdateEffect Hook 来执行 effect 函数

function useDebounceEffect(
  // 执行函数
  effect: EffectCallback,
  // 依赖数组
  deps?: DependencyList,
  // 配置防抖的行为
  options?: DebounceOptions,
) {
  const [flag, setFlag] = useState({});

  const { run } = useDebounceFn(() => {
    setFlag({}); // 设置新的空对象,强制触发更新
  }, options);

  useEffect(() => {
    return run();
  }, deps);

  // 只在 flag 依赖更新时执行,但是会忽略首次执行
  useUpdateEffect(effect, [flag]);
}

完整源码

useThrottleFn

用来处理函数节流的 Hook。

官方文档

基本用法

官方在线 Demo

频繁调用 run,但只会每隔 500ms 执行一次相关函数。

import React, { useState } from 'react';
import { useThrottleFn } from 'ahooks';

export default () => {
  const [value, setValue] = useState(0);
  const { run } = useThrottleFn(
    () => {
      setValue(value + 1);
    },
    { wait: 500 },
  );

  return (
    <div>
      <p style={{ marginTop: 16 }}> Clicked count: {value} </p>
      <button type="button" onClick={run}>
        Click fast!
      </button>
    </div>
  );
};

核心实现

实现原理是调用封装 lodash 的 throttle 方法。

支持的选项,都是 lodash.throttle 里面的参数:

interface ThrottleOptions {
  wait?: number; // 等待时间,单位为毫秒
  leading?: boolean; // 是否在延迟开始前调用函数
  trailing?: boolean; // 是否在延迟开始后调用函数
}
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(`useThrottleFn expected parameter is a function, got ${typeof fn}`);
    }
  }

  // 最新的 fn 节流函数
  const fnRef = useLatest(fn);

  // 默认是 1000 毫秒
  const wait = options?.wait ?? 1000;

  // 节流函数
  const throttled = useMemo(
    () =>
      throttle(
        (...args: Parameters<T>): ReturnType<T> => {
          return fnRef.current(...args);
        },
        wait,
        options,
      ),
    [],
  );

  // 卸载时取消节流函数调用
  useUnmount(() => {
    throttled.cancel();
  });

  return {
    run: throttled, // 触发执行 fn
    cancel: throttled.cancel, // 取消当前节流
    flush: throttled.flush, // 当前节流立即调用
  };
}

完整源码

useThrottleEffect

为 useEffect 增加节流的能力。

官方文档

基本用法

官方在线 Demo

import React, { useState } from 'react';
import { useThrottleEffect } from 'ahooks';

export default () => {
  const [value, setValue] = useState('hello');
  const [records, setRecords] = useState<string[]>([]);
  useThrottleEffect(
    () => {
      setRecords((val) => [...val, value]);
    },
    [value],
    {
      wait: 1000,
    },
  );
  return (
    <div>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Typed value"
        style={{ width: 280 }}
      />
      <p style={{ marginTop: 16 }}>
        <ul>
          {records.map((record, index) => (
            <li key={index}>{record}</li>
          ))}
        </ul>
      </p>
    </div>
  );
};

核心实现

它的实现依赖于 useThrottleFn hook,具体实现逻辑同 useDebounceEffect,只是把 useDebounceFn 换成 useThrottleFn

function useThrottleEffect(
  // 执行函数
  effect: EffectCallback,
  // 依赖数组
  deps?: DependencyList,
  // 配置节流的行为
  options?: ThrottleOptions,
) {
  const [flag, setFlag] = useState({});

  const { run } = useThrottleFn(() => {
    setFlag({}); // 设置新的空对象,强制触发更新
  }, options);

  useEffect(() => {
    return run();
  }, deps);

  // 只在 flag 依赖更新时执行,但是会忽略首次执行
  useUpdateEffect(effect, [flag]);
}

完整源码

本文正在参加「金石计划」

原文链接:https://juejin.cn/post/7214858677173715001 作者:JackySummer

(0)
上一篇 2023年3月27日 上午10:37
下一篇 2023年3月27日 上午10:48

相关推荐

发表回复

登录后才能评论