背景
- 迁徙我的项目中,随处可见
ahooks
的身影,在抱有好奇心以及摸索 React 中 自定义 hook 的最佳实际的过程中。于是便有这篇了ahooks
源码解析系列。 ahooks
中有大量的TS
定义,能够从中汲取到很多的代码设计,疾速上手React+TS
开发模式。- 简略疾速即可上手浏览
ahooks
源码,低耦合性也让代码构造更加清晰,调试者也不须要关注简单的逻辑。
官网文档
一) 介绍
ahooks,发音 [eɪ hʊks],是一套高质量牢靠的 React Hooks 库。在以后 React 我的项目研发过程中,一套好用的 React Hooks 库是必不可少的,心愿 ahooks 能成为您的抉择。
个性
- 易学易用
- 反对 SSR
- 对输入输出函数做了非凡解决,且防止闭包问题
- 蕴含大量提炼自业务的高级 Hooks
- 蕴含丰盛的根底 Hooks
- 应用 TypeScript 构建,提供残缺的类型定义文件
装置
$ npm install --save ahooks
# or
$ yarn add ahooks
# or
$ pnpm add ahooks
应用
import { useRequest } from 'ahooks';
二)拉取ahooks代码
将仓库 ahooks clone 到本地
拉起下来的代码外面会有很多工程化的文件,这里就不会做过多介绍了,因为是即使齐全不懂这些货色,也不障碍你能够轻松的调试ahooks
源码。
在CONTRIBUTING.zh-CN
文件中有其奉献指南和启动我的项目的流程。
pnpm install
pnpm run init
代码运行起来之后就能够在本地看到一份和官网截然不同的文档了。
三)罕用Hook源码解析
目前部门次要采纳的是 Mobx+React+TS
以及自研组件库,对于操作视图层的hooks
这里就不做过多介绍了,感兴趣的能够本人钻研一下~
3.1 useDebounceFn
用来解决防抖函数的 Hook。用法和 debounce
十分相似。
const [value, setValue] = useState(0);
const { run } = useDebounceFn(
() => {
setValue(value + 1);
},
{
wait: 500,
},
);
补充一点:
- 空值合并运算符 ?? ,
a = b ?? c
只有 b 不为null
orundefined
则a = c
,否则a = b
let a = 1
let b = 2
const c = a ?? b // c = 1
b = undefined
const d = b ?? a // d = 1
外围代码
import debounce from 'lodash/debounce';
import { useMemo } from 'react';
import type { DebounceOptions } from '../useDebounce/debounceOptions';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
import { isFunction } from '../utils';
type noop = (...args: any) => any;
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
... 省去局部代码
// 永远应用最新的fn
const fnRef = useLatest(fn);
// 空值校验
const wait = options?.wait ?? 1000;
// 思考一下 这里为什么要应用useMemo来包一层呢 ?
// 其实hook也是一个函数,当组件reRender的时候hook也会从新执行一变,所以须要useMemo来记录曾经保留下来的后果
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;
useLastest.ts
这个 hook 的应用场景目前还没找到很好的答案。如果依照这个 实现,每次获取最新的值,那为什么不间接应用 value 呢?
在和一位大佬探讨后,目前失去的后果就是为了适应某些闭包场景。
import { useRef } from 'react';
function useLatest(value) {
var ref = useRef(value);
ref.current = value;
return ref;
}
export default useLatest;
从useDebounceFn能够学习到的TS编码
type noop = (...args: any) => any;
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions)
在TS中extends关键字能够对传入的进来的范型类型进行限度。
举个简略的🌰
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
...
const debounced = useMemo(
() =>
debounce(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
....
}
Parameters
和ReturnType
能够别离获取TS中函数的入参类型和返回类型。
3.2 useCreation
useCreation是 useMemo
或 useRef
的替代品。
举个简略的🌰
sandbox
这里每次批改count
的值,getRrandomNum
都会被从新执行(执行两次是因为React中的严格模式…)
换成useCreaction
就完满解决了这个问题 sandbox
外围代码
import type { DependencyList } from 'react';
import { useRef } from 'react';
import depsAreSame from '../utils/depsAreSame';
export default function useCreation<T>(factory: () => T, deps: DependencyList) {
const { current } = useRef({
deps,
obj: undefined as undefined | T,
initialized: false,
});
/ *
* 尽管useCreation函数会随着组件的reRender而从新执行
* 然而factory函数只有首次进来或者deps依赖产生扭转才会从新执行
*/
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj as T;
}
depsAreSame
性能:比照两个依赖是否相等
import type { DependencyList } from 'react';
function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
if (oldDeps === deps) return true;
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false;
}
return true;
}
从useCreation中学到的TS技巧
import type ... from
让编译器晓得要导入的内容相对是一种类型。详情见 你不晓得的import type)
<!—->
undefined as undefined | T
当给一个变量值,并且须要限度类型的时候,能够通过 as 类型断言操作。
3.3 useSize
监听 DOM 节点尺寸变动的 Hook。
应用场景: 当某个元素的大小产生扭转时,须要进行一系列操作。
举个🌰: 当咱们应用 echarts
绘制图标的时候就会呈现这样的问题。当 echarts
的容器的大小是自适应单位,如rem vw
等。咱们心愿绘制进去的图标也能够追随容器大小扭转而扭转。
const size = useSize(EchartsDomRef);
useEffect(() => {
if (chartRef.current) {
chartRef.current.resize(); // 当容器宽度产生扭转的时候,调用resize办法从新渲染echarts
}
}, [size.width]);
useEffect(() => {
chartRef.current = echarts.init(EchartsDomRef.current);
}, []);
外围代码
import ResizeObserver from 'resize-observer-polyfill';
import useRafState from '../useRafState';
import type { BasicTarget } from '../utils/domTarget';
import { getTargetElement } from '../utils/domTarget';
import useIsomorphicLayoutEffectWithTarget from '../utils/useIsomorphicLayoutEffectWithTarget';
type Size = { width: number; height: number };
function useSize(target: BasicTarget): Size | undefined {
const [state, setState] = useRafState<Size>();
useIsomorphicLayoutEffectWithTarget(
() => {
const el = getTargetElement(target);
if (!el) {
return;
}
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { clientWidth, clientHeight } = entry.target;
setState({
width: clientWidth,
height: clientHeight,
});
});
});
resizeObserver.observe(el);
return () => {
resizeObserver.disconnect();
};
},
[],
target,
);
return state;
}
export default useSize;
3.3 useUnmountedRef
获取以后组件是否曾经卸载的 Hook。
应用场景: 发送网络申请前/后,判断组件是否曾经销毁。如果销毁勾销本次申请/缩小后续的一系列操作。
举个🌰
const unMounted = useUnmountedRef();
getData.then(res => {
if (unMounted.current) {
return;
}
....一些列耗时的操作
})
外围代码
这个hook的实现很简略。在组件挂载在dom上的时候设置值false
,当组件销毁的时候设置为true
。
import { useEffect, useRef } from 'react';
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
return () => {
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
export default useUnmountedRef;
3.4 useMemoizedFn
长久化 function 的 Hook,实践上,能够应用 useMemoizedFn 齐全代替 useCallback。
外围代码
应用过 Vue3
的肯定不会生疏WatchEffect
的主动收集依赖机制;
这里实现也很奇妙,re-render 阶段不断更新 fn.Ref.current
的援用值。 然而memoizedFn.current
指向的函数返回值就是函数 fn 最新的返回值,同时memoizedFn
只会在挂载阶段赋值一次,这样就确保了memoizedFn.current
的援用地址放弃不变。
import { useMemo, useRef } from 'react';
type noop = (this: any, ...args: any[]) => any;
type PickFunction<T extends noop> = (
this: ThisParameterType<T>,
...args: Parameters<T>
) => ReturnType<T>;
function useMemoizedFn<T extends noop>(fn: T) {
const fnRef = useRef<T>(fn);
// why not write fnRef.current = fn?
// https://github.com/alibaba/hooks/issues/728
fnRef.current = useMemo(() => fn, [fn]);
const memoizedFn = useRef<PickFunction<T>>();
if (!memoizedFn.current) {
memoizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current as T;
}
3.5 useLockFn
用于给一个异步函数减少 竞 态锁,避免并发执行。
应用场景: 在许多场景中都能够应用useLockFn
来缩小网络上的开销。
举个简略的🌰 如果当初有个上拉加载的函数loadMore
。这个函数须要进行网络申请才会返回最终的后果。个别的做法就是通过设置isLoading
状态来判断函数是否执行。然而有了useLockFn
咱们的代码就会变得繁难许多,业务逻辑也会变得更加清晰。
const loadMore = async () => {
if(isLoading) return
isLoading = true
const data = await getMockData(...parasms)
isLoading = false
}
外围代码
实现也非常简单,就是利用了useRef
在整个生命周期只会初始化一次,来记录一个状态变量,判断 fn
是否执行结束。
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) => {
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],
);
}
但这里有个小问题 ?大家晓得为什么这里须要应用useCallback
来包一层嘛,而不是间接返曾经解决了 竞态🔒 逻辑的函数呢?
答案也很简略,当传入函数fn并不是一个长期函数,而是一个援用。当fn的援用地址未产生扭转的时候,就避免了useLockFn函数返回后果产生扭转,有可能会造成子组件的re-render。
四)Other hook
4.1 useUpdateEffect
useUpdateEffect
用法等同于 useEffect
,然而会疏忽首次执行,只在依赖更新时执行。
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;
};
}, []);
// update
hook(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
const useUpdateEffect = createUpdateEffect(useEffect);
首次进入函数会执行两次 hook 这里其实就是useEffect
设置状态变量isMounted
为true
,接下来每次更新就间接执行effect
函数。
4.2 useSetState
治理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState
基本一致。
import { useCallback, useState } from 'react';
import { isFunction } from '../utils';
export type SetState<S extends Record<string, any>> = <K extends keyof S>(
state: Pick<S, K> | null | ((prevState: Readonly<S>) => Pick<S, K> | S | null),
) => void;
const useSetState = <S extends Record<string, any>>(
initialState: S | (() => S),
): [S, SetState<S>] => {
const [state, setState] = useState<S>(initialState);
// 入参为函数的时候间接执行函数,失去返回值再与旧值扩大合并。
const setMergeState = useCallback((patch) => {
setState((prevState) => {
const newState = isFunction(patch) ? patch(prevState) : patch;
return newState ? { ...prevState, ...newState } : prevState;
});
}, []);
return [state, setMergeState];
};
4.3 usePrevious
保留上一次状态的 Hook。个别用于缓存状态。
import { useRef } from 'react';
export type ShouldUpdateFunc<T> = (prev: T | undefined, next: T) => boolean;
const defaultShouldUpdate = <T>(a?: T, b?: T) => !Object.is(a, b);
function usePrevious<T>(
state: T,
shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,
): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>();
// 进行 shallow equal 比拟
if (shouldUpdate(curRef.current, state)) {
prevRef.current = curRef.current;
curRef.current = state;
}
return prevRef.current;
}
四)总结
最初!!!学习 ahooks 肯定是你 React 老手进阶最好的源码库。
第一篇对于 Ahooks源码解析 就这样完结了,感觉useRequest
和useUrlState
这两个 hook 能够独自拿进去讲讲。期待下一篇吧。
发表回复