本篇文章是解读 ahooks@3.8.0 源码系列的第七篇 - Advanced,欢迎您的指正和点赞。
本文主要解读 useControllableValue、useCreation、useEventEmitter、useIsomorphicLayoutEffect、useLatest、useMemoizedFn、useReactive 的源码实现。
useControllableValue
文档地址
详细代码
受控组件和非受控组件的解释:
受控组件:在 HTML 中,表单元素 (如 <input>
、<textarea>
、<select>
等) 通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态 (mutable state) 通常保存在组件的 state 属性中,并且只能通过 setSate 来更新。
对于受控组件,输入的值始终由 React 的 state 驱动,你可以将 value 传递给其它 UI 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码。
使用非受控组件,表单数据将交由 DOM 节点来处理,可以使用 ref 来从 DOM 节点中获取表单数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
| import { useMemo, type SetStateAction, useRef } from "react";
import useUpdate from "../useUpdate";
import { isFunction } from "@/utils";
import useMemoizedFn from "../useMemoizedFn";
export interface Options<T> {
defaultValue?: T;
defaultValuePropName?: string;
valuePropName?: string;
trigger?: string;
}
export type Props = Record<string, any>;
export interface StandardProps<T> {
value: T;
defaultValue?: T;
onChange: (val: T) => void;
}
function useControllableValue<T = any>(
props: StandardProps<T>
): [T, (v: SetStateAction<T>) => void];
function useControllableValue<T = any>(
props?: Props,
options?: Options<T>
): [T, (v: SetStateAction<T>, ...args: any[]) => void];
function useControllableValue<T = any>(
props: Props = {},
options: Options<T> = {}
) {
const {
defaultValue,
defaultValuePropName = "defaultValue",
valuePropName = "value",
trigger = "onChange",
} = options;
const value = props[valuePropName] as T;
// 如果 props 中存在值的属性名,则为受控组件
const isControlled = props.hasOwnProperty(valuePropName);
// 初始值
const initialValue = useMemo(() => {
// 受控组件
if (isControlled) {
return value;
}
// props defaultValue
if (props.hasOwnProperty(defaultValuePropName)) {
return props[defaultValuePropName];
}
// options defaultValue
return defaultValue;
}, []);
const stateRef = useRef(initialValue);
// 如果是受控组件,则由外部传入的 value 来更新 state
if (isControlled) {
stateRef.current = value;
}
const update = useUpdate();
function setState(v: SetStateAction<T>, ...args: any[]) {
const r = isFunction(v) ? v(stateRef.current) : v;
// 如果是非受控组件,则手动更新状态,强制组件重新渲染
if (!isControlled) {
stateRef.current = r;
update();
}
// 触发 trigger
if (props[trigger]) {
props[trigger](r, ...args);
}
}
return [stateRef.current, useMemoizedFn(setState)] as const;
}
export default useControllableValue;
|
useCreation
文档地址
详细代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import depsAreSame from "@/utils/depsAreSame";
import { useRef, type DependencyList } from "react";
const 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;
};
export default useCreation;
|
useEventEmitter
文档地址
详细代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| import { useEffect, useRef } from "react";
type Subscription<T> = (val: T) => void;
export class EventEmitter<T> {
// 订阅器列表
private subscriptions = new Set<Subscription<T>>();
// 推送事件
emit = (val: T) => {
for (const subscription of this.subscriptions) {
subscription(val);
}
};
// 订阅事件
useSubscription = (callback: Subscription<T>) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const callbackRef = useRef<Subscription<T>>();
callbackRef.current = callback;
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
function subscription(val: T) {
if (callbackRef.current) {
callbackRef.current(val);
}
}
// 组件创建时自动注册订阅
this.subscriptions.add(subscription);
// 组件销毁时自动取消订阅
return () => {
this.subscriptions.delete(subscription);
};
}, []);
};
}
const useEventEmitter = <T = void,>() => {
const ref = useRef<EventEmitter<T>>();
if (!ref.current) {
ref.current = new EventEmitter();
}
return ref.current;
};
export default useEventEmitter;
|
useIsomorphicLayoutEffect
在 SSR 模式下,使用 useLayoutEffect 时,会出现以下警告
⚠️ Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer’s output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.
为了避免该警告,可以使用 useIsomorphicLayoutEffect 代替 useLayoutEffect。
useIsomorphicLayoutEffect 源码如下:
1
2
3
4
5
6
| import { useEffect, useLayoutEffect } from "react";
import isBrowser from "../../../utils/isBrowser";
const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
|
在非浏览器环境返回 useEffect,在浏览器环境返回 useLayoutEffect。
更多信息可以参考 useLayoutEffect and SSR
useLatest
文档地址
详细代码
1
2
3
4
5
6
7
8
9
10
| import { useRef } from "react";
const useLatest = <T,>(value: T) => {
const ref = useRef(value);
// 拿到最新值
ref.current = value;
return ref;
};
export default useLatest;
|
useMemoizedFn
文档地址
详细代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| import { isFunction } from "@/utils";
import isDev from "@/utils/isDev";
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>;
const useMemoizedFn = <T extends noop>(fn: T) => {
if (isDev) {
if (!isFunction(fn)) {
console.error(
`useMemoizedFn expected parameter is a function, got ${typeof fn}`
);
}
}
// 每次拿到最新的 fn,把它更新到 fnRef,保证 fnRef 能够持有最新的 fn 引用
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) {
// 每次调用时,都能拿到最新的 args
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current as T;
};
export default useMemoizedFn;
|
useReactive
文档地址
详细代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| import { useRef } from "react";
import useCreation from "../useCreation";
import useUpdate from "../useUpdate";
import { isPlainObject } from "lodash";
// 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);
// https://github.com/alibaba/hooks/issues/1317
const descriptor = Reflect.getOwnPropertyDescriptor(target, key);
// 属性不可读且不可写,直接返回原始的属性值
if (!descriptor?.configurable && !descriptor?.writable) {
return res;
}
// Only proxy plain object or array,
// otherwise it will cause: https://github.com/alibaba/hooks/issues/2080
// 属性值是普通对象或数组,调用 observer 方法对其观察,并返回观察后的结果;否则直接返回原始的属性值
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;
}
const useReactive = <S extends Record<string, any>>(initialState: S) => {
const update = useUpdate();
const stateRef = useRef<S>(initialState);
const state = useCreation(() => {
return observer(stateRef.current, () => {
update();
});
}, []);
return state;
};
export default useReactive;
|