ahooks源码解析 提高你写自定义hook的能力
前言:ahooks, 是阿里开源的一套高质量可靠的 React Hooks 库。由于想提高一下写自定义hooks的能力,所以去看了一下ahooks的实现。
比较通用的hook
useMemoizedFn
持久化 function 的 Hook,功能就是保持返回函数的引用不变,并使得回调始终为最新
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
//这能保证fn为最新的,每次更新组件,传入最新的fn,同时更新fnRef
fnRef.current = useMemo(() => fn, [fn]);
const memoizedFn = useRef<PickFunction<T>>();
//如果没有值,则把函数的,如果有则跳过,保证memoizedFn不会被重复赋值,保持最初的函数引用
if (!memoizedFn.current) {
memoizedFn.current = function (this, ...args) {
//在最初的函数中调用fn,相当于做了一层代理
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current as T;
}
通过 fnRef 保存最新的函数,然后通过memoizedFn 这个ref保持函数的引用不变, 应用场景
memoizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args);
};
// 如果这个改成直接return fn.apply(),例:
memoizedFn.current = function (this, ...args) {
return fn.apply(this,args)
};
//这样会使得fn不是最新的,每次函数组件执行,传入最新的fn,都会因为前面的if (!memoizedFn.current) 判断而跳过赋值,所以需要用fnRef作为中转
useUpdateEffect
useUpdateEffect 用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行。
typescript
const useUpdateEffect=createUpdateEffect(useEffect);
useUpdateLayoutEffect
useUpdateLayoutEffect 用法等同于 useLayoutEffect,但是会忽略首次执行,只在依赖更新时执行。
const useUpdateLayoutEffect= createUpdateEffect(useLayoutEffect);
上面这两个api都依赖createUpdateEffect
//createUpdateEffect
export const createUpdateEffect=(hook) => (effect, deps) => {
const isMounted = useRef(false);
// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
用 isMounted 来判断是否是第一次调用effect 的回调,第一次不执行回调,更新isMounted状态。后续再执行回调
LifeCycle
useMount
只在组件初始化时执行的 Hook。
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.();
}, []);
};
通过useEffect ,dep传空数组,实现只调用一次。
`useEffect初次执行时,会走mountEffect方法,useEffect会给组件的fiber打上标记,当组件在beforeMutation 阶段,通过scheduleCallback异步调用flushPassiveEffects,进而调度useEffect回调。
而当组件更新时,走的是updateEffect方法,这个方法内会比较遍历dep,新旧dep不一致时才会更新,当传入空数组,则直接跳过遍历,不更新。
注意!在react 18 , createRoot创建严格模式下,当依赖项为零时, useEffect在严格模式下被调用两次。
回调执行:create => destory =>create
Strict Effects 会先挂载组件,再销毁组件,然后再挂载组件。以确保清理实际上清理了效果并且不会留下一些副作用。
问题讨论:Adding Reusable State to StrictMode
unmount
在组件卸载(unmount)时执行的 Hook。
const useUnmount = (fn: () => void) => {
const ref = useRef(fn);
ref.current = fn;
useEffect(
() => () => {
fnRef.current();
},
[],
);
};
为什么用 ref 挂载呢?是为了解决闭包产生的问题。
由于传的是空数组,当组件挂载后,回调执行,形成的 destroy 函数,挂载到effect对象上。此后,这个useEffect函数的回调不会再次执行,也就是这个destroy函数不会更新,可是外部传入的 fn 是会改变,每次函数组件执行,可能会生成一个新的 fn 。如果挂载fn 到ref 中,则可以访问到最新的 fn ,因为 ref 的引用不会改变。
useUnmountedRef
获取当前组件是否已经卸载的 Hook。
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
return () => {
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
destroy 函数执行的时候,就把状态更改true,回调执行的时候就是更改 false
State
useSetState
管理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致。
const useSetState = (initialState)=>{
const [state, setState] = useState(initialState);
const setMergeState = useCallback((patch) => {
setState((prevState) => {
const newState = isFunction(patch) ? patch(prevState) : patch;
return newState ? { ...prevState, ...newState } : prevState;
});
}, []);
return [state, setMergeState];
};
setState在底层处理逻辑上主要是和老 state 进行合并处理,会有一层浅合并,而 useState 则是会重新赋值。
这个useSetState就是帮你做了一层合并
useToggle
用于在两个状态值间切换的 Hook。
function useToggle(defaultValue, reverseValue) {
const [state, setState] = useState(defaultValue);
//封装一个行为对象
const actions = useMemo(() => {
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue)
const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value) => setState(value);
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);
return {
toggle,
set,
setLeft,
setRight,
};
// useToggle ignore value change
// },[defaultValue, reverseValue]);
}, []);
return [state, actions];
}
注意!这个hook会忽略值的变化,因为 useMemo使用的依赖项为空数组
useBoolean
优雅的管理 boolean 状态的 Hook。
export default function useBoolean(defaultValue = false): [boolean, Actions] {
const [state, { toggle, set }] = useToggle(defaultValue);
const actions: Actions = useMemo(() => {
const setTrue = () => set(true);
const setFalse = () => set(false);
return {
toggle,
set: (v) => set(!!v),
setTrue,
setFalse,
};
}, []);
return [state, actions];
}
这个hook基于useToggle
useCookieState
一个可以将状态存储在 Cookie 中的 Hook 。
import Cookies from 'js-cookie';
function useCookieState(cookieKey: string, options: Options = {}) {
const [state, setState] = useState<State>(() => {
//初始化的时候,从cookie取值
const cookieValue = Cookies.get(cookieKey);
if (isString(cookieValue)) return cookieValue;
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
});
//useMemoizedFn 功能就是保持返回函数的引用不变,并使得回调始终为最新
const updateState = useMemoizedFn(
(
newValue: State | ((prevState: State) => State),
newOptions: Cookies.CookieAttributes = {},
) => {
//新旧的配置项合并取值,并setState回调中操作cookie的值
const { defaultValue, ...restOptions } = { ...options, ...newOptions };
setState((prevState) => {
const value = isFunction(newValue) ? newValue(prevState) : newValue;
if (value === undefined) {
Cookies.remove(cookieKey);
} else {
Cookies.set(cookieKey, value, restOptions);
}
return value;
});
},
);
return [state, updateState] as const;
}
useLocalStorageState
将状态存储在 localStorage 中的 Hook 。
import isBrowser from '../utils/isBrowser';
const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));
useSessionStorageState
将状态存储在 sessionStorage 中的 Hook。
import isBrowser from '../utils/isBrowser';
const useSessionStorageState = createUseStorageState(() =>
isBrowser ? sessionStorage : undefined,
);
这两个hook都基于 createUseStorageState 以及浏览器环境判断
浏览器环境判断
//浏览器环境判断,要有全局window变量,window变量有document属性,document属性有createElement属性
export const isUndef = (value: unknown): value is undefined => typeof value === 'undefined';
const isBrowser = !!(!isUndef(window) && window.document && window.document.createElement);
createUseStorageState
export function createUseStorageState(getStorage: () => Storage | undefined) {
//根据不同的storage传值,返回这个hook函数
function useStorageState<T>(key: string, options?: Options<T>) {
let storage: Storage | undefined;
// https://github.com/alibaba/hooks/issues/800
try {
storage = getStorage();
} catch (err) {
console.error(err);
}
//序列化函数
const serializer = (value: T) => {
//配置项有自定义的序列化函数就调用自定义的函数,没有就调用JSON.stringify进行序列化
if (options?.serializer) {
return options?.serializer(value);
}
return JSON.stringify(value);
};
//反序列化函数
const deserializer = (value: string) => {
//配置项有自定义的反序列化函数就调用自定义的函数,没有就调用JSON.parse解析
if (options?.deserializer) {
return options?.deserializer(value);
}
return JSON.parse(value);
};
//取值函数
function getStoredValue() {
try {
const raw = storage?.getItem(key);
//如果storage里这个value值,则进行反序列化
if (raw) {
return deserializer(raw);
}
} catch (e) {
console.error(e);
}
if (isFunction(options?.defaultValue)) {
return options?.defaultValue();
}
return options?.defaultValue;
}
const [state, setState] = useState<T | undefined>(() => getStoredValue());
//这个hook,功能是只有依赖项更新时才会执行回调,会跳过第一次的初始化执行
useUpdateEffect(() => {
//当key改变的时候,同步改变state状态
setState(getStoredValue());
}, [key]);
//更新状态函数
const updateState = (value?: T | IFuncUpdater<T>) => {
if (isUndef(value)) {
setState(undefined);
storage?.removeItem(key);
} else if (isFunction(value)) {
const currentState = value(state);
try {
setState(currentState);
storage?.setItem(key, serializer(currentState));
} catch (e) {
console.error(e);
}
} else {
try {
setState(value);
storage?.setItem(key, serializer(value));
} catch (e) {
console.error(e);
}
}
};
//useMemoizedFn下面讲
return [state, useMemoizedFn(updateState)] as const;
}
return useStorageState;
}
注意!当浏览器完全禁用cookie时,使用storage会报错 : Failed to read the 'localStorage' property from 'Window': Access is denied for this document. 所以使用了try catch包裹 getStorage
当用户浏览器完全禁用cookie时,浏览器会禁用部分api,各个浏览器会有差异。
在这些浏览器中禁用 cookie 会禁用以下功能:
- Chrome:cookies、localStorage、sessionStorage、IndexedDB
- 火狐:cookies、localStorage、sessionStorage
- IE:仅 cookie
详情请看:
禁用 cookie 时 useLocalStorageState 会抛出错误 和 Can Session storage / local storage be disabled and Cookies enabled?
...hook比较多,后续补充