本文是 ahooks 源码(v3.7.4)系列的第十篇——Effect 篇(一)
往期文章:
本文主要解读 useUpdateEffect
、useUpdateLayoutEffect
、useAsyncEffect
、useDebounceFn
、useDebounceEffect
、useThrottleFn
、useThrottleEffect
的源码实现
useUpdateEffect
用法等同于 useEffect
,但是会忽略首次执行,只在依赖更新时执行。
API 与 React.useEffect 完全一致。
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>
);
};
里面其实是实现了 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
用法等同于 useLayoutEffect
,但是会忽略首次执行,只在依赖更新时执行。
API 与 React.useLayoutEffect 完全一致。
使用上与 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);
useEffect 支持异步函数。
组件加载时进行异步的检查
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>
);
};
在使用 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(async () => {
(async function getData() {
const data = await fetchData();
})()
}, [])
useEffect(() => {
const getData = async () => {
const data = await fetchData();
};
getData()
}, [])
const getData = async () => {
const data = await fetchData();
};
useEffect(() => {
getData()
}, [])
useAsyncEffect 就是一种实现了,省略上面那些代码处理
function useAsyncEffect(
effect: () => AsyncGenerator | Promise,
deps: DependencyList
);
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?
用来处理防抖函数的 Hook。
频繁调用 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, // 当前防抖立即调用
};
}
为 useEffect 增加防抖的能力。
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]);
}
用来处理函数节流的 Hook。
频繁调用 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, // 当前节流立即调用
};
}
为 useEffect 增加节流的能力。
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]);
}
阅读量:1010
点赞量:0
收藏量:0