11.React-hooks-自定义钩子
# 0.前言
代码仓库地址:
# 问题 1:useMemo
如何避免大规模计算
此函数为 useMemo
和 useRef
的合并版。
举例:使用 usePow([1,2,3])
,只会产生一次计算。
const usePow = (list: number[]) => {
return useMemo(
() =>
list.map((item: number) => {
return Math.pow(item, 2);
}),
[],
);
};
2
3
4
5
6
7
8
9
# 问题 2:改进 useRef
useRef
的通常的作用:
- 解决闭包问题。
- 缓存引用地址。
存在的问题:
没有初始化状态
// useState 支持接受一个函数 const [value, setValue] = useState(() => {}); // 错误写法 useRef 不支持 const valueRef = useRef(() => { return 0; });
1
2
3
4
5
6
7实例化过程会多次执行
class Foo { constructor() { this.data = Math.random(); } data: number; } // new Foo 每次均会执行 const valueRef = useRef(new Foo()); // 如果能实现: const valueRef = useRef(() => new Foo());
1
2
3
4
5
6
7
8
9
10
11
12
解决方案
如果要解决初始化问题,很简单,封装下面这个钩子即可:
const useRef = <T>(defaultValue: T | (() => T)) => {
const objRef = useRef();
// 首次加载
useFirstMount(() => {
objRef.current =
typeof defaultValue === "function" ? defaultValue() : defaultValue;
});
return objRef;
};
2
3
4
5
6
7
8
9
10
上述改造后,无法实现 useRef
缓存引用地址的功能,如下:
const useLatest = <T>(value: T) => {
const ref = useRef(value);
ref.current = value;
return ref;
};
2
3
4
5
因此,最终的解决方案,就是设计一个新的 hooks
:useCreation
。该自定义钩子会在后续章节单独介绍
const valueRef = useCreation(() => new Foo(), []);
完整示例:
export function useCreation<T>(callback: () => T, deps: any[]) {
// 缓存 函数返回值;
const objRef = useRef<T>(null);
// 是否首次加载
const firstMountRef = React.useRef(true);
// 初始化 deps
const depsRef = useRef(deps);
/* 首次加载 */
if (firstMountRef.current) {
// 初始化结束后
firstMountRef.current = false;
// 由于 useRef(()=> callback()) 不支持函数初始化赋值。
objRef.current = callback();
}
/* 更新阶段 */
if (!firstMountRef.current && !depsAreSame(depsRef.current, deps)) {
// 更新 deps 依赖
depsRef.current = deps;
objRef.current = callback();
}
return objRef;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
注:这边返回值稍有不同,这里为
objRef
,而非objRef.current
。
# 问题 3:如何缓存函数的引用地址?
正常情况下,我们缓存一个函数的引用地址非常麻烦。
特别是处理 useCallback
的依赖数组。
import { useEvent } from "./useEvent";
import React, { useCallback, useState } from "react";
const Demo: React.FC<any> = React.memo((props) => {
/* 环境变量 */
const [count, setCount] = useState(0);
// ❎ 错误案例:存在闭包问题
const onClick = useCallback(() => {
alert(count);
}, []); /* 缺少 count 变量*/
const onClick2 = useCallback(() => {
alert(count);
}, [count]);
const onClick3 = useEvent(() => {
alert(count);
});
return (
<>
<button onClick={() => setCount(count + 1)}>count+1</button>
<p>{count}</p>
<button onClick={onClick}>闭包现象</button>
<button onClick={onClick2}>useCallback 依赖数组</button>
<button onClick={onClick3}>useEvent</button>
</>
);
});
export default Demo;
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
点击
<button>
闭包现象,函数体内的环境变量始终停止在0
(定义时的变量),即产生闭包现象。
现在希望封装一个钩子,省略掉依赖数组,类似于如下钩子:
rc-util
中的useEvent
ahooks
中的useMemoizedFn
特别是在自定义 useXXX
钩子中,一定特别注意对入参为 fn
函数,且当此函数定义时使用到了外部变量时极其容易产生闭包,因此在函数设计时一定要使用 useRef
缓存。
// onChange 函数
const [envVar, setEnvVar] = useState();
const onChange = () => {
console.log(envVar);
};
function useXXX(params) {
// 注:当 onChange 函数定义时,使用到了外部变量 envVar
const { onChange } = params;
const onChangeFn = useEvent(onChange);
const onChnageFn = useMemoizedFn(onChange);
useEffect(() => {
onChange("xxxx");
return () => {
onChange("yyyy");
};
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
useEvent
、useMemoizedFn
的源码部分虽然不难,但是设计的非常巧妙。
对于函数来说,可以通过下面这种方式无线层的包裹。
const fn1 = (arg1, arg2) => {};
// 对入参 arg1, arg2 继承
export const fn2 = (arg1, arg2) => {
fn1(arg1, arg2);
};
2
3
4
5
在 js
编程中不会这么写,但是对于 hooks
编程来说,正好可以利用到这多包裹一层的箭头函数。
为避免重复渲染,可以使用 useCallback
缓存函数对包裹的箭头函数进行缓存,这样既解决了 deps
的问题,也可以锁死引用地址。
var envVar; /* 环境变量 */
// fn1 引用地址发生变化
const fn1 = (arg1, arg2) => {
/* 使用到了 envVar */
};
// fn2 固定引用地址
export const fn2 = React.useCallback((arg1, arg2) => fn1(arg1, arg2), []);
2
3
4
5
6
7
8
9
useEvent
完整写法:
function useEvent(callback) {
// 始终缓存最新值,以下也可简写为 useLatest(callback);
const fnRef = useRef<any>();
fnRef.current = callback;
const memoFn = React.useCallback<T>(
(...args: any) => fnRef.current?.(...args) as any,
[],
);
return memoFn;
}
2
3
4
5
6
7
8
9
10
11
12
# 问题 4:如何处理 useLayoutEffect
在 SSR
环境?
在 SSR
环境下,不支持 useLayoutEffect
自定义钩子,需进行降级 useLayoutEffect
→ useEffect
。
在 SSR
无法使用 DOM
,因此可以封装 canUseDom
来判断当前环境:
function canUseDom() {
return !!(
typeof window !== "undefined" &&
window.document &&
window.document.createElement
);
}
2
3
4
5
6
7
这里
!!()
将window.document.createElement
进行转化。
const useLayoutEffect =
process.env.NODE_ENV !== "test" && canUsedom()
? React.useLayoutEffect
: React.useEffect;
2
3
4
# 问题 4:useUpdateEffect
注:此钩子函数使用到的
useLayoutEffect
最好都做下降级兼容性处理。
首次更新阶段自定义 hooks
:
- 同步钩子:
useLayoutUpdateEffect
- 异步钩子:
useUpdateEffect
最简单的封装方式:
function useLayoutUpdateEffect = (callback,deps){
const firstMountRef = React.useRef(true);
// 将 useLayoutEffect 换成 useEffect 即可变为 useUpdateEffect
useLayoutEffect(()=>{
if(firstMountRef.current){
// mount 阶段
firstMountRef.current = false;
}else {
// update 阶段
callback();
};
}, deps);
}
2
3
4
5
6
7
8
9
10
11
12
13
更进一步考虑到,可以返回 firstMountRef
判断当前组件是否已经卸载。
function useLayoutUpdateEffect = (callback,deps){
// 是否首次更新
const firstMountRef = React.useRef(true);
// 将 useLayoutEffect 换成 useEffect 即可变为 useUpdateEffect
useLayoutEffect(()=>{
if(!firstMountRef.current){
return callback();
}
}, deps);
// 支持 unMount 处理
useLayoutEffect(()=>{
firstMountRef.current = false;
return ()=>{
firstMountRef.current = true;
}
},[]);
return firstMountRef;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
测试案例:
import { useBoolean, useUnmountedRef } from "ahooks";
import { message } from "antd";
import React, { useEffect } from "react";
const MyComponent = () => {
const unmountedRef = useUnmountedRef();
useEffect(() => {
setTimeout(() => {
if (!unmountedRef.current) {
message.info("component is alive");
}
}, 3000);
}, []);
return <p>Hello World!</p>;
};
export default () => {
const [state, { toggle }] = useBoolean(true);
return (
<>
<button type="button" onClick={toggle}>
{state ? "unmount" : "mount"}
</button>
{state && <MyComponent />}
</>
);
};
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
# 问题 5:useMount
\ useUnMount
- 首次加载触发钩子:
const useMount = (callback: () = >void ) => {
useEffect(()=>{
// mount 阶段
callback?.();
},[]);
}
2
3
4
5
6
- 卸载时触发
这个需要注意一个点,就是使用
useRef
来确保所传入的函数为最新的状态
const useUnmount = (callback: () => void) => {
// 初始化执行
const funRef = useRef();
// 每次函数执行
funRef.current = callback;
// 这里涉及闭包
useEffect(
() => () => {
funRef.current?.();
},
[],
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
以上实现基于
useEffect
实现,同样可以实现useLayoutEffect
版本
const useLayoutEffect =
process.env.NODE_ENV !== "test" && canUsedom()
? React.useLayoutEffect
: React.useEffect;
const useLayoutMount = (callback: () => void) => {
useLayoutEffect(() => {
// mount 阶段
callback?.();
}, []);
};
const useLayoutUnmount = (callback: () => void) => {
const funRef = useRef(callback);
funRef.current = callback;
useLayoutEffect(
() => () => {
funRef.current?.();
},
[],
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
额外说明的是可以使用 useRef
封装一个初始化钩子:
const useFirstMount = (callback: () => void) => {
const isFirstMount = useRef(true);
if (isFirstMount.current) {
callback();
isFirstMount.current = false;
}
};
2
3
4
5
6
7
综合测试案例
function TestComp() {
useFirstMount(() => {
console.log("useFirstMount-首次加载");
});
useMount(() => {
console.log("useMount-首次加载");
});
useLayoutMount(() => {
console.log("useLayoutMount-首次加载");
});
useUnmount(() => {
console.log("useUnMount-组件卸载");
});
useLayoutUnmount(() => {
console.log("useLayoutUnmount-组件卸载");
});
return "Hello world";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 问题 6:useUpdate
如何强制刷新页面
const [, update] = useState({});
update({});
2
3
封装成钩子:
function useUpdate() {
const [, update] = useState({});
return useCallback(() => update({}), []);
}
2
3
4
# 问题 7:useCreation
= useMemo
+ useRef
此钩子使用方式基本等同于 useMemo
,使用场景有限。
该钩子可用于学习,如 从零实现一个 useMemo
官方自定义钩子。
在
React
官网文档中说,被memo
的值一定不会被重计算。
const res = useMemo(() => {
return xxx;
}, [deps]);
const res = useCreation(() => {
return xxx;
}, [deps]);
2
3
4
5
6
7
版本 1: ahooks
写法
// 通过 Object.is 比较依赖数组的值是否相等
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;
}
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; // 初始化标识为 true
}
return current.obj as T;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
版本 2: 自己的写法,可以把状态变化更清晰分离出来
- 完全使用
useRef
实现,需手动实现deps
对比。
export function useCreation<T>(callback: () => T, deps: any[]) {
// 缓存 函数返回值;
const objRef = useRef<T>(null);
// 是否首次加载
const firstMountRef = React.useRef(true);
// 初始化 deps
const depsRef = useRef(deps);
/* 首次加载 */
if (firstMountRef.current) {
// 初始化结束后
firstMountRef.current = false;
// 由于 useRef(()=> callback()) 不支持函数初始化赋值。
objRef.current = callback();
}
/* 更新阶段 */
if (!firstMountRef.current && !depsAreSame(depsRef.current, deps)) {
// 更新 deps 依赖
depsRef.current = deps;
objRef.current = callback();
}
return objRef.current;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
版本 3: 封装 useUpdateEffect
和 useFirstUpdate
简化更新阶段写法。
🌟 这里特别注意,返回值为
objRef.current
,因此函数中只能使用同步hooks
操作。首次加载只可以使用:
useFirstMount
,切忌不要使用useMount(async)
、useLayoutMount(sync)
首次更新只可以使用:
useFisrtUpdate
,切忌不要使用useUpdateEffec(async)
、useUpdateLayoutEffect(sync)
const useFisrtUpdate = (callback: any) => {
const isFirstMount = useRef(true);
if (isFirstMount.current) {
isFirstMount.current = false;
} else {
callback();
}
};
// ❎ 错误使用
const useLayoutMount = (callback: any) => {
const isFirstMount = useRef(true);
// useEffect(async) useLayoutEffect(async)
useLayoutEffect(() => {
if (isFirstMount.current) {
isFirstMount.current = false;
} else {
callback();
}
});
};
export function useCreation<T>(callback: () => T, deps: any[]) {
// 缓存 函数返回值;
const objRef = useRef<T | undefined>();
// 初始化 deps
const depsRef = useRef(deps);
/* 首次加载 */
useFirstMount(() => {
objRef.current = callback();
});
/* 更新阶段 */
useFisrtUpdate(() => {
if (!depsAreSame(depsRef.current, deps)) {
// 更新 deps 依赖
depsRef.current = deps;
objRef.current = callback();
}
});
return objRef.current;
}
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
版本 4: 直接使用 useEffect
或者 useLayoutEffect
自带的 deps
监听,简单封装即可。
export function useCreation<T>(callback: () => T, deps: any[]) {
// 缓存 函数返回值;
const objRef = useRef<T | undefined>();
// 标志位
const [, refresh] = useState({});
useEffect(() => {
objRef.current = callback();
refresh({});
}, deps);
return objRef.current;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 问题 8:useMergeState
封装
rc-util
库 useMergedState
函数的使用,基础使用不再赘述。
API
设计:
function useMergedState<T, R = T>(
defaultStateValue: T | (() => T),
option?: {
defaultValue?: T | (() => T);
value?: T;
onChange?: (value: T, prevValue: T) => void;
postState?: (value: T) => T;
},
): [R, Updater<T>] {
// 省略逻辑代码......
}
2
3
4
5
6
7
8
9
10
11
# 问题 9:跨层级通信(事件发布订阅 hooks
) + 单例模式
涉及 hooks: useEventEmitter
首先:实现一个 EventEmitter
类,用于存储 subscriptions
数组, ahooks
考虑的比较简单,省略了 eventName
这个字段,因此整体 hooks
功能比较轻量。
import { useEffect, useRef } from "react";
type Subscription<T> = (val: T) => void;
export class EventEmitter<T> {
// 采用Set存储订阅回调
private subscriptions = new Set<Subscription<T>>();
// 遍历回调函数
emit = (val: T) => {
for (const subscription of this.subscriptions) {
subscription(val);
}
};
/* 自定义 Hooks 可以定义在 Class 结构上 */
useSubscription = (callback: Subscription<T>) => {
const callbackRef = useRef<Subscription<T>>();
callbackRef.current = callback;
useEffect(() => {
// 订阅
function subscription(val: T) {
if (callbackRef.current) {
callbackRef.current(val);
}
}
/* 类似:触发 on 事件 */
this.subscriptions.add(subscription);
return () => {
/* 类似:触发 emit 事件 */
this.subscriptions.delete(subscription);
};
}, []);
};
}
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
保证实例始终是单例:
/* 单例模式写法 */
export default function useEventEmitter<T = void>() {
const ref = useRef<EventEmitter<T>>();
if (!ref.current) {
ref.current = new EventEmitter();
}
return ref.current;
}
2
3
4
5
6
7
8
使用方案:
使用
Context
或props
顶部缓存。
import { useRef, FC } from "react";
import useEventEmitter from "./useEventEmitter";
import { EventEmitter } from "./useEventEmitter";
const MessageBox: FC<{
focus: EventEmitter<void>;
}> = function(props) {
const { focus } = props;
return (
<button onClick={() => /* emit 触发事件 */ focus.emit()}> emit </button>
);
};
const InputBox: FC<{
focus: EventEmitter<void>;
}> = function(props) {
const inputRef = useRef<any>();
const { focus } = props;
/* on 事件 */
focus.useSubscription(() => {
inputRef.current.focus();
});
return <input ref={inputRef} />;
};
export default function() {
/* 保证单例 */
const focus = useEventEmitter();
return (
<>
<MessageBox focus={focus} />
<InputBox focus={focus} />
</>
);
}
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
此
hooks
过于简单,若希望支持eventName
,可参考这篇文章实现 《React 中优雅的使用 useEventEmitter 进行多组件通信》 (opens new window)
# 问题 10:如何处理函数的返回值为卸载函数
# 问题 11: useComposeRef
的写法
当存在多个 ref
绑定到 dom
结构时,可以使用 ref
透传函数
import React, { useRef } from "react";
const Demo: React.FC<any> = React.memo((props) => {
const formRef1 = useRef<any>();
const formRef2 = useRef<any>();
return (
<>
<div
ref={(node) => {
formRef1.current = node;
formRef2.current = node;
}}
>
Hello World
</div>
<button
onClick={() => {
console.log("formRef1", formRef1.current);
console.log("formRef2", formRef2.current);
}}
>
获取 ref 对象
</button>
</>
);
});
export default Demo;
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
此时,也可以使用 rc-util
暴露出的 useComposeRef
自定义钩子或 composeRef
函数。
使用方式如下:
const formRef1 = useRef<any>();
const formRef2 = useRef<any>();
// 以下两种使用方式等价:
const mergedRef = useComposeRef(formRef1, formRef2);
const mergedRef2 = useMemo(()=>{
return composeRef(formRef1,formRef2);
}, [formRef1,formRef2])
// 对应的 DOM 结构绑定
<div ref={mergedRef}></div>
2
3
4
5
6
7
8
9
10
11
完整示例
import React, { useRef } from "react";
import { useComposeRef } from "./useComposeRef";
const Demo: React.FC<any> = React.memo((props) => {
const formRef1 = useRef<any>();
const formRef2 = useRef<any>();
// useComposeRef 或者 composeRef 额外支持一种函数写法,(node)=>{}
const mergedRef = useComposeRef(formRef1, (node) => {
console.log("node", node);
formRef2.current = node;
});
return (
<>
<div ref={mergedRef}>Hello World</div>
<button
onClick={() => {
console.log("formRef1", formRef1.current);
console.log("formRef2", formRef2.current);
}}
>
获取 ref 对象
</button>
</>
);
});
export default Demo;
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
源码实现:
export function composeRef<T>(...refs: React.Ref<T>[]): React.Ref<T> {
// 过滤非空
const refList = refs.filter(Boolean);
// 处理只有1个情况
if (refList.length <= 1) {
return refList[0];
}
return (node: T) => {
refs.forEach((ref) => {
// ref 支持接受如:(node)=>{ domRef.current = node} 形式写法
if (typeof ref === "function") {
ref(node);
} else if (typeof ref === "object" && ref && "current" in ref) {
(ref as any).current = node;
}
});
};
}
// 对 composeRef 做了一个 memo 处理
export function useComposeRef<T>(...refs: React.Ref<T>[]): React.Ref<T> {
return useMemo(() => composeRef(...refs), refs);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 问题 12:useAsyncEffect
# 问题13:usePrevious
通过 useEffect
只能监听到某一个值的变化,而无法监听值变化的方向。
const App = ({initialState})=>{
const {isLogin} = initialState;
useEffect(()=>{
// 只希望 isLogin: false → true 触发某个操作
},[isLogin])
}
2
3
4
5
6
可以直接使用 ref
对需要监听的变量进行操作:
const App = ({initialState})=>{
const {isLogin} = initialState;
const preRef = useRef(isLogin);
const curRef = useRef(isLogin);
// 更新状态
preRef.current = curRef.current;
curRef.current = isLogin;
if(preRef.currrent === false &&
curRef.current === true
){
// 触发更新操作
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
进一步可以将上述过程封装成钩子 usePrevious
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>();
// 嵌入更新逻辑控制,是否修改 preRef.current
if (shouldUpdate(curRef.current, state)) {
prevRef.current = curRef.current;
curRef.current = state;
}
return prevRef.current; // shouldUpdate 返回 true 为 preRef 为最新值
}
export default usePrevious;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
使用时:
const App = ({initialState})=>{
const {isLogin} = initialState;
const _isLogin = usePrevious(isLogin,(preState,curState)=>{
if(preState === false && curState === true){
// 触发更新操作
}
return true;
})
}
2
3
4
5
6
7
8
9