useUpdateEffect、useUpdateLayoutEffect
这两个 hook 在之前的文章里面说过,不过还是拿出来再走一遍吧
//入口函数为 createUpdateEffect
//useUpdateEffect 传入 useEffect
export default createUpdateEffect(useEffect);
//useUpdateLayoutEffect 传入 useLayoutEffect
export default createUpdateEffect(useLayoutEffect);
//代码实现
export const createUpdateEffect: (
hook: effectHookType,
) => effectHookType = hook => (effect, deps) => {
const isMounted = useRef(false);
// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
// 首次执行完时候,设置为 true,从而下次依赖更新的时候可以执行逻辑
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
这两个 hook 都通过 createUpdateEffect 入口函数,只不过一个传 useEffect,一个传 useLayoutEffect 作为参数。他们和 useEffect、useLayoutEffect 的用法一样,只不过这两个 hook 会忽略首次执行
思路:
- 内部通过
useRef初始化一个标识isMounted,用来判断组件是否挂载 - 然后执行
hook(也就是useEffect、useLayoutEffect),当组件首次挂载时,会执行更改标识isMounted.current = true,也就是忽略了首次执行 - 当依赖发生改变时,在执行回调
effect
useAsyncEffect
useAsyncEffect 支持传入一个异步的 callback
我们知道 useEffect 是不允许传入异步的 callback 的,比如:
useEffect(async () => {
await fetch(url)
}, [])
这样写是会报错的,因为 useEffect 会在重新渲染时,清除上一次的副作用,或者在组件销毁时,清除副作用,它应该返回一个 cleanup 函数,而不是返回 Promise,这会导致 React 在调用销毁函数的时候报错,因为当返回值是异步时,无法预知代码的执行情况,所以可能导致一些难以发现的 bug
从源码的角度来看这个问题的话:
function commitHookEffectListUnmount(tag, finishedWork) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
var destroy = effect.destroy;
effect.destroy = undefined;
// 这里!!!!
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
从源码中可以看出,卸载的时候会通过函数组件对应的 fiber 获取 effect 链表,然后遍历链表,获取环链上的每一个节点,如果 destroy 不是 undefined 就执行,所以如果 useEffect 第一个参数传入 async, 那么这里的 destroy 就是一个 promise 对象,对象是不能执行的,所以报错。
那我们如果非要在 useEffect 里面使用异步函数怎么办呢?两种方式
- 方式一:定义一个函数用来接受异步函数返回的 Promise
useEffect(() => { const asyncFun = async () => { //do something }; asyncFun(); }, []); - 方式二:立即执行函数
useEffect(() => { (async () => { // do something })(); }, []);
但是在看 useAsyncEffect 的源码前,我们先来认识一下 Gnerator
Gnerator
Generator 是一个返回值 iterator 对象的函数。而 iterator 叫做迭代器,它要满足迭代器协议:一个拥有 next 方法的对象,且 next 方法要返回形如 {done: boolean, value: any} 的对象,其中 done 用来表示当前的数据结构是否遍历完毕,value 是每一次遍历的值
那 iterator 有什么用?我们知道,像 Array 这样的数据结构我们是可以去遍历、拓展的,因为它默认实现了 Symbol.iterator 属性,该属性如果被定义了,就可以被遍历、拓展,比如:
let arr = ['a','b','c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
我们手动通过为数组 arr 定义 Symbol.iterator 属性,是个函数,调用它时会返回迭代器,也就是 iter,然后通过 next 方法,就可以遍历 arr 了
对于对象来说也是如此:
let obj = {a: "hello", b: "world"};
// 自定义迭代器
function createIterator(items) {
let keys = Object.keys(items);
let i = 0;
return {
next: function () {
let done = (i >= keys.length);
let value = !done ? items[keys[i++]] : undefined;
return {
value: value,
done: done,
};
}
};
}
let iterator = createIterator(obj); // 返回迭代器
console.log(iterator.next()); // "{ value: 'hello', done: false }"
console.log(iterator.next()); // "{ value: 'world', done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
我们再回到 Generator 来。刚刚说了,Generator 是用于返回 iterator 迭代器的函数,它的定义如下:
//Generator 定义时要在 function 后加一个星号
function* generator() {
yield 111
yield 222
};
let iterator = generator(); //返回迭代器
iterator.next() // {value: 111, done: false}
iterator.next() // {value: 222, done: false}
iterator.next() // {value: undefined, done: true}
Generator 内部通过关键字 yield,可以像 打断点 一样,暂停函数的执行,并且可以从上一次暂停的位置继续往后执行,因此 Generator 可以返回一系列的值,然后像”同步“代码一样执行
这里还有个 Generator 的孪生兄弟,叫做 Symbol.asyncIterator,它两差不多,只不过后者用来指定一个对象的异步迭代器,可用于 for await ... of 循环。比如:
const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function*() {
yield "hello";
yield "async";
yield "iteration!";
};
(async () => {
for await (const x of myAsyncIterable) {
console.log(x);
// expected output:
// "hello"
// "async"
// "iteration!"
}
})();
看到这里,对 Generator 有了个基本的认识,接下来就来看 useAsyncEffect 源码
useAsyncEffect 源码
function isAsyncGenerator(
val: AsyncGenerator<void, void, void> | Promise<void>,
): val is AsyncGenerator<void, void, void> {
return isFunction(val[Symbol.asyncIterator]);
}
function useAsyncEffect(
effect: () => AsyncGenerator<void, void, void> | Promise<void>,
deps?: DependencyList,
) {
useEffect(() => {
const e = effect();
let cancelled = false;
async function execute() {
if (isAsyncGenerator(e)) {
while (true) {
const result = await e.next();
if (result.done || cancelled) {
break;
}
}
} else {
await e;
}
}
execute();
return () => {
cancelled = true;
};
}, deps);
}
export default useAsyncEffect;
它的思路为:
- 先执行传入的
effect()异步回调函数,将其的返回值赋值给e - 然后通过内部
isAsyncGenerator方法 判断e是否是异步Generator,如果是,就通过可迭代对象的next方法执行,直到done: false完毕为止;如果不是,那就是个Promise,直接await e - 最后,内部定义了一个
cancelled变量,用来中断执行的
因为可能会遇到一些场景:用户在执行 a 操作,但是 a 操作还没完成时,就开始下一轮的 b 操作,那此时 a 操作是没有意义的,可以直接停止往后执行。直接跳过 a 到 b 操作去。这个 cancelled 就是干这个事情的
useUpdate
这个 hook 之前的文章也讲过,这里就一笔带过
useUpdate 会返回一个函数,调用该函数会强制组件重新渲染
import { useCallback, useState } from 'react';
const useUpdate = () => {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
};
export default useUpdate;
很简单,就是当我们调用返回的函数时,useUpdate 内部会通过 setState 更改状态,然后就促使组件重新渲染
useDebounceEffect
该 hook 为 useEffect 增加防抖的能力。
function useDebounceEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: DebounceOptions,
) {
// 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调
const [flag, setFlag] = useState({});
// 通过 lodash 的 防抖对 setFlag 包装一层防抖效果
const { run } = useDebounceFn(() => {
setFlag({});
}, options);
// 首次执行
useEffect(() => {
// 每当过了 options.wait 时间后,设置 flag 改变标识
return run();
}, deps);
// 当标识改变时,执行 effect 回调函数,但忽略首次执行
useUpdateEffect(effect, [flag]);
}
export default useDebounceEffect;
思路如下:
- 设置
flag标识符变量,然后对setFlag函数做一个防抖(用的是 lodash 的防抖包装了一层) - 首次执行时不影响
- 后续执行时,当
flag通过options.wait时间改变后,才执行useUpdateEffect执行effect回调。注意:useUpdateEffect忽略首次执行,所以代码前面用的是 useEffect
useThrottleEffect
为 useEffect 增加节流的能力。
它也是通过 lodash.throttle 方法来实现的
function useThrottleEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: ThrottleOptions,
) {
// 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调
const [flag, setFlag] = useState({});
// 用来处理函数节流的 Hook。
const { run } = useThrottleFn(() => {
setFlag({});
}, options);
useEffect(() => {
return run();
}, deps);
// 只有在 flag 变化的时候,才执行 effect 函数
useUpdateEffect(effect, [flag]);
}
思路和 useDebounceEffect 一样
useDebounceFn
useDebounceFn 是用来处理防抖函数的 hook
它主要也是使用的 lodash 的 debounce 方法
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
const fnRef = useLatest(fn);
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,
cancel: debounced.cancel,
flush: debounced.flush,
};
}
export default useDebounceFn;
思路:
- 用
useLatest保证每次都拿到最新的 fn - 用 lodash 的 debounce 包一层,内部调用函数时是执行
fnRef.current(...args) - 然后返回被包装后的函数 debounced,外部调用时会调用它
- 然后在组件卸载时会取消 debounce
useThrottleFn
useThrottleFn 是用来处理函数节流的 Hook。
主要用到的是 lodash 的 throttle 方法
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const throttled = useMemo(
() =>
// 最终都是调用了 lodash 的节流函数
throttle(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
useUnmount(() => {
throttled.cancel();
});
return {
run: throttled,
// 取消
cancel: throttled.cancel,
// 立即调用
flush: throttled.flush,
};
}
它的思路和 useDebounceFn 一样
useDeepCompareEffect、useDeepCompareLayoutEffect
useDeepCompareEffect、useDeepCompareLayoutEffect 用法与 useEffect、useLayoutEffect 一致,只不过在依赖发生改变时,使用的是 lodash.isEqual 方法进行深比较
// 入口函数都是 createDeepCompareEffect,只不过一个接受 useEffect 作为参数
// 一个接受 useLayoutEffect 作为参数
export default createDeepCompareEffect(useEffect);
// 源代码
export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => {
const ref = useRef<DependencyList>();
const signalRef = useRef<number>(0);
if (deps === undefined || !depsEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}
// hook 就是 useEffect / useLayoutEffect
// effect 就是传入的 callback 回调
hook(effect, [signalRef.current]);
};
思路:
- 通过
ref记录上一次的依赖项 - 然后判断如果本次依赖是
undefined或者 两次依赖不同发生了改变,就记录本次依赖ref.current = deps, - 然后更新
signalRef.current,从而触发hook中的回调
这两个 hook 的思路是一样的,上面拿的是 useDeepCompareEffect 来做分析
useInterval 和 useTimeout
他俩源码一样,只不过一个用的 setInterval,一个用的 setTimeout
这里拿 useInterval 分析:
const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {
const timerCallback = useMemoizedFn(fn);
const timerRef = useRef<NodeJS.Timer | null>(null);
//清除函数
const clear = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
}, []);
useEffect(() => {
if (!isNumber(delay) || delay < 0) {
return;
}
if (options.immediate) {
//立即执行
timerCallback();
}
// 计时器
timerRef.current = setInterval(timerCallback, delay);
// 下一次渲染时,清除上一次定时器
return clear;
}, [delay, options.immediate]);
return clear;
};
export default useInterval;
思路:
- 首先检验
delay,不通过直接return - 然后判断是否立即执行,如果立即执行则就调用
timerCallback - 不管是不是立即执行,都开起定时器
- 然后利用 useEffect 的
cleanup函数机制,在下一次渲染时清除上一次的定时器
跟 setInterval 的区别如下:
- 可以支持第三个参数,通过 immediate 能够立即执行我们的定时器。
- 在变更 delay 的时候,会自动清除旧的定时器,并同时启动新的定时器。
- 通过 useEffect 的返回清除机制,开发者不需要关注清除定时器的逻辑,避免内存泄露问题。这点是很多开发者会忽略的点。
useTimeout 思路跟它一样
useLockFn
useLockFn 用于给一个异步函数增加竞态锁,防止并发执行
function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) {
const lockRef = useRef(false);
return useCallback(
async (...args: P) => {
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],
);
}
export default useLockFn;
思路:
- 内部通过
lockRef来做一个竞态锁的标识,初始化为false - 当执行传入的
fn时请求时,lockRef.current设置为true - 当重复请求时,此时竞态锁标识为
true,直接 return,不执行原函数,从而达到加锁的目的
结语
从这几期的源码学习下来,我们发现对于传入的参数,一些会通过 useRef、useLatest存下来,然后当某些逻辑的执行需要条件判断时,基本会把判断条件用 useRef 初始化,所以以后自定义 hook 的时候可以参照这样的方式来
以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论。