ahooks源码系列(六):功能性相关的 hook
useMap
useMap
是用于管理 Map 类型状态的 Hook。
Object 和 Map 很类似。它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值等等
但是,在一些场景下,使用 Map 是更优的选择,以下是一些常见的点:
键的类型
:一个 Map 的键可以是任意类型
,包括函数、对象或任意基本类型。一个 Object 的键必须是一个String
或是Symbol
。键值的顺序
:Map 中的键是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。虽然 Object 的键目前是有序的,但并不总是这样,而且这个顺序是复杂的。因此,最好不要依赖属性的顺序。Size
:Map 通过size()
属性获取长度,而 Object 的键值对个数只能手动计算,比如 Object.keys(object)性能
:Map 在频繁增删键值对的场景下表现更好。Object 在频繁添加和删除键值对的场景下未作出优化。
源码如下:
function useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {
// 以函数返回值的形式初始化 Map,便于 reset 操作
const getInitValue = () => new Map(initialValue);
const [map, setMap] = useState<Map<K, T>>(getInitValue);
const set = (key: K, entry: T) => {
setMap((prev) => {
const temp = new Map(prev);
temp.set(key, entry);
return temp;
});
};
const setAll = (newMap: Iterable<readonly [K, T]>) => {
setMap(new Map(newMap));
};
const remove = (key: K) => {
setMap((prev) => {
const temp = new Map(prev);
temp.delete(key);
return temp;
});
};
const reset = () => setMap(getInitValue());
const get = (key: K) => map.get(key);
return [
map,
{
set: useMemoizedFn(set),
setAll: useMemoizedFn(setAll),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
get: useMemoizedFn(get),
},
] as const;
}
export default useMap;
我们拆开来看:
- 首先,内部通过
getInitValue
来记录初始值,便于后续reset
操作 - 然后内部就是定义各种方法:
set
:拿到之前的 Map 的内容,然后创建新 Map,并在新 Map 里面新增 key-valueremove
:同样,拿到之前的 Map 的内容,然后创建新 Map,并在新 Map 里面移除对应的 key,如果成功返回 true,否则返回 false(它是在删除后,通过 Map.prototype.has(key) 判断这个 key 是否还存在的)setAll
:传入一个新的 Map,覆盖原本的 Mapreset
:重置为内容是initialValue
的 Map,直接调用getInitValue()
就能创建一个新的 Map,内容是 initialValueget
:通过 Map.get 方法获取 key 对应的 value 值,如果不存在,返回 undefined
还是比较简单的,其中在返回这些方法的时候,都通过 useMemoizedFn
包了一层,这个 hook 能保证 fn 的地址不变,且每次能拿到最新的 state
useSet
useSet
是管理 Set 类型状态的 Hook。
代码还是挺简单的,和 useMap 差不多,只不过 useSet 在 新增(元素唯一
)、删除时需要判断当前值是否存在
function useSet<K>(initialValue?: Iterable<K>) {
const getInitValue = () => new Set(initialValue);
const [set, setSet] = useState<Set<K>>(getInitValue);
const add = (key: K) => {
if (set.has(key)) {
return;
}
setSet((prevSet) => {
const temp = new Set(prevSet);
temp.add(key);
return temp;
});
};
const remove = (key: K) => {
if (!set.has(key)) {
return;
}
setSet((prevSet) => {
const temp = new Set(prevSet);
temp.delete(key);
return temp;
});
};
const reset = () => setSet(getInitValue());
return [
set,
{
add: useMemoizedFn(add),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
},
] as const;
}
export default useSet;
还是拆开来看:
- 首先,内部还是通过
getInitValue
来记录初始值,便于后续reset
操作 - 然后内部同样定义各种方法
add
:先判断当前要加入的元素是否已经存在于 Set 里面,有就直接 return,否则就拿到之前的 Set 的内容,然后创建新 Set,并在新 Set 里面新增值remove
:同样,先判断当前要移除的元素是否存在于 Set 里面,不存在就 return,否则就拿到之前的 Set 的内容,然后创建新 Set,并在新 Set 里面移除值reset
:重置为内容是initialValue
的 Set,直接调用getInitValue()
就能创建一个新的 Set,内容是 initialValue
useUrlState
useUrlState
通过 url query 来管理 state 的 Hook。
该 Hooks 基于 react-router
的 useLocation & useHistory & useNavigate 进行 query 管理,所以使用该 Hooks 之前,你需要保证
-
你项目正在使用
react-router
5.x 或 6.x 版本来管理路由 -
独立安装了 @ahooksjs/use-url-state(因为 useUrlState 是独立一个仓库进行管理的)
然后源码里面引用了 query-string
这个第三方
import { parse, stringify } from 'query-string';
其中,parse 方法将URL解析成对象的形式, stringify 方法将对象 序列化成URL的形式,以&进行拼接,详细的可以自己去百度看看,这里不是重点
// ...
import * as tmp from 'react-router';
// ...
const useUrlState = <S extends UrlState = UrlState>(
// 初始状态
initialState?: S | (() => S),
// url 配置
options?: Options,
) => {
type State = Partial<{ [key in keyof S]: any }>;
const {
// 状态变更时切换 history 的方式
navigateMode = 'push',
// query-string parse 的配置
parseOptions,
// query-string stringify 的配置
stringifyOptions,
} = options || {};
const mergedParseOptions = { ...baseParseConfig, ...parseOptions };
const mergedStringifyOptions = {
...baseStringifyConfig,
...stringifyOptions,
};
// useLocation钩子返回表示当前URL的location对象。您可以将它想象成一个useState,它在URL更改时返回一个新值。
const location = rc.useLocation();
// https://v5.reactrouter.com/web/api/Hooks/usehistory
// useHistory 钩子可以访问用来导航的历史实例。
// react-router v5
const history = rc.useHistory?.();
// react-router v6
const navigate = rc.useNavigate?.();
const update = useUpdate();
const initialStateRef = useRef(
typeof initialState === 'function'
? (initialState as () => S)()
: initialState || {},
);
// 根据 url query
const queryFromUrl = useMemo(() => {
return parse(location.search, mergedParseOptions);
}, [location.search]);
const targetQuery: State = useMemo(
() => ({
...initialStateRef.current,
...queryFromUrl,
}),
[queryFromUrl],
);
// 省略部分代码(设置 url 状态)
return [targetQuery, useMemoizedFn(setState)] as const;
};
首先这是初始化部分:
initialState
为初始状态,options
为 url 的配置,包括状态变更时切换 history 的方式、query-string parse 和 stringify 的配置。- 通过 react-router 的 useLocation 获取到 URL 的 location 对象。
- react-router 兼容 5.x 和 6.x,
5.x 使用 useHistory,6.x 使用 useNavigate
。 queryFromUrl
是调用 query-string 的parse
方法,将 location 对象的 search 处理成对象值。targetQuery
就是 queryFromUrl 和初始值进行 merge 之后的结果。
然后再看如何去更新 url
// 设置 url 状态
const setState = (s: React.SetStateAction<State>) => {
const newQuery = typeof s === 'function' ? s(targetQuery) : s;
// 1. 如果 setState 后,search 没变化,就需要 update 来触发一次更新。比如 demo1 直接点击 clear,就需要 update 来触发更新。
// 2. update 和 history 的更新会合并,不会造成多次更新
update();
if (history) {
history[navigateMode]({
hash: location.hash,
search:
stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) ||
'?',
});
}
if (navigate) {
navigate(
{
hash: location.hash,
search:
stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) ||
'?',
},
{
replace: navigateMode === 'replace',
},
);
}
};
- 首先是根据传入的
s
,获取到新的状态newQuery
- 然后根据 react-router 的版本调用不同的方法进行更新。
- 假如是 5.x 版本,调用 useHistory 方法,更新对应的状态。
- 假如是 6.x 版本,调用 useNavigate 方法,更新对应的状态。
- 通过 update() (也就是
useUpdate()
) 来更新 url 状态。
useCookieState
useCookieState
是可以将状态存储在 Cookie 中的 Hook 。
内部使用了 js-cookie
第三方
function useCookieState(cookieKey: string, options: Options = {}) {
// 初始化状态
const [state, setState] = useState<State>(() => {
const cookieValue = Cookies.get(cookieKey);
if (isString(cookieValue)) return cookieValue;
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
});
// 更新状态
const updateState = useMemoizedFn(
(
newValue: State | ((prevState: State) => State),
newOptions: Cookies.CookieAttributes = {},
) => {
const { defaultValue, ...restOptions } = { ...options, ...newOptions };
const value = isFunction(newValue) ? newValue(state) : newValue;
setState(value);
if (value === undefined) {
Cookies.remove(cookieKey);
} else {
Cookies.set(cookieKey, value, restOptions);
}
},
);
return [state, updateState] as const;
}
代码分成两部分:状态初始化
和 状态更新
,先看第一部分:
- 如果 cookie 里面已经有这个值,直接拿出来,然后判断其类型
- 如果是字符串,直接返回
- 如果是函数,则调用函数,返回结果
- 如果都不满足,则返回
options.defaultValue
第二部分:
- 将
newOptions
与 useCookieState 设置的options
合并。restOptions
会透传给 js-cookie 的 set 方法的第三个参数。 - 然后判断传入的
newValue
,假如是函数,则取执行后返回的结果,否则直接取该值。 - 如果值为 undefined,则清除 cookie。否则,调用 js-cookie 的 set 方法。
- 最终返回 state、 updateState
useLocalStorageState、useSessionStorageState
这两个 hook 是将状态存储在 localStorage 和 sessionStorage 中的 Hook 。
他们都通过 createUseStorageState 入口函数实现的
const useLocalStorageState = createUseStorageState(() =>
isBrowser ? localStorage : undefined
);
const useSessionStorageState = createUseStorageState(() =>
isBrowser ? sessionStorage : undefined,
);
这里以 useLocalStorageState 为例:
export interface Options<T> {
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
defaultValue?: T | IFuncUpdater<T>;
onError?: (error: unknown) => void;
}
export function createUseStorageState(getStorage: () => Storage | undefined) {
function useStorageState<T>(key: string, options: Options<T> = {}) {
let storage: Storage | undefined;
// 从传入的 options 里拿自定义的 onError 错误处理回调
const {
onError = (e) => {
console.error(e);
},
} = options;
// https://github.com/alibaba/hooks/issues/800
// getStorage 返回 store 的时候可能是 undefined,此时就直接 try catch 抛出错误
try {
storage = getStorage();
} catch (err) {
onError(err);
}
// 获取传入的自定义序列化方法,如果没有就 JSON.stringify(value);
const serializer = (value: T) => {
if (options?.serializer) {
return options?.serializer(value);
}
return JSON.stringify(value);
};
// 获取传入的自定义反序列化方法,如果没有就 JSON.parse(value);
const deserializer = (value: string): T => {
if (options?.deserializer) {
return options?.deserializer(value);
}
return JSON.parse(value);
};
// 获取 storage 中对应的 key-value, try catch 包裹一层来抛出错误
function getStoredValue() {
try {
const raw = storage?.getItem(key);
if (raw) {
return deserializer(raw);
}
} catch (e) {
onError(e);
}
// 如果没有 raw,就返回默认值,
if (isFunction(options?.defaultValue)) {
return options?.defaultValue();
}
return options?.defaultValue;
}
const [state, setState] = useState(() => getStoredValue());
// 传入 key 时,重新设置 state
useUpdateEffect(() => {
setState(getStoredValue());
}, [key]);
// 更新 storage 函数
const updateState = (value?: T | IFuncUpdater<T>) => {
const currentState = isFunction(value) ? value(state) : value;
setState(currentState);
// 如果值是 undefined,移除该项
if (isUndef(currentState)) {
storage?.removeItem(key);
} else {
try {
// 否则就设置到 state 中
storage?.setItem(key, serializer(currentState));
} catch (e) {
console.error(e);
}
}
};
// 最后返回
return [state, useMemoizedFn(updateState)] as const;
}
return useStorageState;
}
思路注释在代码中,跟着代码看会好懂很多
useCreation
useCreation
是 useMemo
或 useRef
的替代品。
因为 useMemo
不能保证被 memo 的值一定不会被重计算,而 useCreation
可以保证这一点。以下为 React 官方文档中的介绍:
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without
useMemo
— and then add it to optimize performance.
而相比于 useRef
,你可以使用 useCreation
创建一些常量,这些常量和 useRef
创建出来的 ref 有很多使用场景上的相似,但对于复杂常量的创建,useRef
却容易出现潜在的性能隐患。
const a = useRef(new Subject()); // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了
const b = useCreation(() => new Subject(), []); // 通过 factory 函数,可以避免性能隐患
源码如下:
export default function useCreation<T>(factory: () => T, deps: DependencyList) {
const { current } = useRef({
deps,
obj: undefined as undefined | T,
initialized: false,
});
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj as T;
}
实现原理是基于 useRef 再加一层判断:
- 初始化时,
current.initialized
是初始化标识,初始值为 false,此时记录初始deps
、执行工厂函数factory()
、设置初始化标识为 true - 然后当后续 deps 发生改变时,重复上述步骤
useEventEmitter
useEventEmitter
适合的是在距离较远的组件之间进行事件通知,或是在多个组件之间共享事件通知。
主要是通过发布订阅设计模式实现。
export class EventEmitter<T> {
private subscriptions = new Set<Subscription<T>>();
// 推送事件
emit = (val: T) => {
// 触发 subscriptions 中所有的事件
for (const subscription of this.subscriptions) {
subscription(val);
}
};
useSubscription = (callback: Subscription<T>) => {
// 存储当前事件的回调
const callbackRef = useRef<Subscription<T>>();
callbackRef.current = callback;
useEffect(() => {
function subscription(val: T) {
if (callbackRef.current) {
callbackRef.current(val);
}
}
//订阅事件
this.subscriptions.add(subscription);
return () => {
// 通过 useEffect 的 cleanup 函数机制去删除订阅的事件
this.subscriptions.delete(subscription);
};
}, []);
};
}
export default function useEventEmitter<T = void>() {
const ref = useRef<EventEmitter<T>>();
// 如果没有 EventEmitter 实列对象,就创建
if (!ref.current) {
ref.current = new EventEmitter();
}
// 返回 EventEmitter 实列对象
return ref.current;
}
class EventEmitter 类中维护了 subscriptions Set
结构存放事件列表,useSubscription
订阅事件,通过 emit
方法推送事件。熟悉发布订阅模式的会很好懂
注意:在组件多次渲染时,每次渲染调用
useEventEmitter
得到的返回值会保持不变,不会重复创建EventEmitter
的实例。 这是个单例模式
useReactive
useReactive
提供一种数据响应式的操作体验,定义数据状态不需要写useState
,直接修改属性即可刷新视图。
实现原理跟 Vue3 类似,通过 Proxy
进行数据劫持和修改。
源码:
function useReactive<S extends Record<string, any>>(initialState: S): S {
const update = useUpdate();
const stateRef = useRef<S>(initialState);
const state = useCreation(() => {
return observer(stateRef.current, () => {
update();
});
}, []);
return state;
}
可以发现其实最主要的是在 observer
方法里面劫持 stateRef.current
// k:v 原对象:代理过的对象
const proxyMap = new WeakMap();
// k:v 代理过的对象:原对象
const rawMap = new WeakMap();
function observer<T extends Record<string, any>>(initialVal: T, cb: () => void): T {
const existingProxy = proxyMap.get(initialVal);
// 添加缓存 防止重新构建proxy
if (existingProxy) {
return existingProxy;
}
// 防止代理已经代理过的对象
// https://github.com/alibaba/hooks/issues/839
if (rawMap.has(initialVal)) {
return initialVal;
}
const proxy = new Proxy<T>(initialVal, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// Only proxy plain object or array,
// otherwise it will cause: https://github.com/alibaba/hooks/issues/2080
return isPlainObject(res) || Array.isArray(res) ? observer(res, cb) : res;
},
set(target, key, val) {
const ret = Reflect.set(target, key, val);
cb();
return ret;
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
cb();
return ret;
},
});
proxyMap.set(initialVal, proxy);
rawMap.set(proxy, initialVal);
return proxy;
}
思路:
- 首先判断当前这个
initialVal
是不是已经被代理过了,如果被代理了直接返回,做了个缓存的机制 - 然后通过
WeakMap
避免重复代理,也就是代理已经代理过的对象 - 然后通过
Proxy
代理initialVal
,内部通过Reflect
去操作,在set
、deleteProperty
操作后,会调用cb
(useUpdate)强制刷新组件 - 最后记录当前被代理的对象,然后返回 proxy
结语
以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论
转载自:https://juejin.cn/post/7251398450699583549