React Hooks深入使用心得

不要类比 class 组件里的生命周期

useEffect(fn, [])只会执行一次,但是不完全等于componentDidMount生命周期。如果将这两种一一对比,对 Effect 的使用就很难提升了,踩坑之后的领悟。

useEffect 使用

  1. 把不依赖 props 和 state 的函数提到你的组件外面
  2. 把那些仅被 effect 使用的函数放到 effect 里面
  3. 如果这样做了以后,你的 effect 还是需要用到组件内的函数,包括 props 传进来的函数,则使勇敢 useCallback 包一层

出现无限重复请求的情况

  1. useEffect 没有设置第二个参数依赖数组,即useEffect(fn),则每一次浏览器渲染后都会执行 useEffect,然后在 effect 中更新了状态引起渲染并再次触发 effect;
  2. 设置的依赖数组总是在变化,useEffect(fn, [a, b, c]),比如函数引用会导致无限循环,解决办法是 1)把函数放到 effect 里,2)把函数提到组件外面,3)用 useCallBack 包裹,4)也可以使用 useMemo 处理

Effect拿到的总是定义它的那次渲染中的props和state,如果你觉得在渲染中拿到了一些旧的 props 和 state,且不是你想要的,那么很大可能就是你遗漏里一些 useEffect 的依赖项,导致了 bug,可以通过 eslint-plugin-react-hooksexhaustive-deps 规则来提出警告并修改,避免因漏了依赖产的 bug。

useEffect 执行时机

浅浅的看下执行过程:
/packages/react/src/ReactHooks.js
function useEffect

  1. 获取 dispatcher 对象dispatcher = resolveDispatcher()
    ReactCurrentDispatcher.current,这个值要么 null | Dispatcher

Dispatcher 是这么一个对象

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
export type Dispatcher = {|
getCacheForType?: <T>(resourceType: () => T) => T,
readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean
): T,
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S
): [S, Dispatch<A>],
useContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean
): T,
useRef<T>(initialValue: T): {| current: T |},
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void,
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null
): void,
useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
useImperativeHandle<T>(
ref: {| current: T | null |} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null
): void,
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
useDeferredValue<T>(value: T): T,
useTransition(): [(() => void) => void, boolean],
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>
): Snapshot,
useOpaqueIdentifier(): any,
useCacheRefresh?: () => <T>(?() => T, ?T) => void,

unstable_isNewReconciler?: boolean,
|};
  1. 执行 useEffect(/packages/react-reconciler/src/ReactFiberHooks.new.js)
    给当前的 currentHookNameInDev = 'useEffect';赋值

    let currentHookNameInDev: ?HookType = null;

    通过 mountHookTypesDev 方法,将当前 hook push 到 hookTypesDev 这个数组里,

    hookTypesDev 定义:let hookTypesDev: Array<HookType> | null = null;,这个列表存储初始化 render 时 hook 的调用顺序

  2. 检查 checkDepsAreArrayDev 依赖是否合法

  3. 执行 mountWorkInProgressHook,返回 workInProgressHook

  4. 设置 currentlyRenderingFiber$1.effectTag |= fiberEffectTag;

    currentlyRenderingFiber 表示 work-in-progress fiber

  5. 设置 hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps)

pushEffect 做了什么:

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
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
  1. mountIndeterminateComponent 挂载好 jsx 后,将 isRendering=false,然后经过一系列调度、协调(这个过程太长了,没理解透。。。),在 commitHookEffectListMount 执行了 effect 里传进的回调

自定义 hook 使用

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

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
const useDataApi = (initialUrl, initialData) => {
const [data, setData] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);

useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);

try {
const result = await axios(url);

setData(result.data);
} catch (error) {
setIsError(true);
}

setIsLoading(false);
};

fetchData();
}, [url]);

return [{ data, isLoading, isError }, setUrl];
};

函数式组件和类组件的区别

函数式组件和类组件的最大区别在于:函数式组件捕获了渲染所用的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function MessageThread() {
const [message, setMessage] = useState("");

const showMessage = () => {
alert("You said: " + message);
};

const handleSendClick = () => {
setTimeout(showMessage, 3000);
};

const handleMessageChange = (e) => {
setMessage(e.target.value);
};

return (
<>
<input value={message} onChange={handleMessageChange} />
<button onClick={handleSendClick}>Send</button>
</>
);
}

如果我发送一条特定的消息,组件不应该对实际发送的是哪条消息感到困惑。这个函数组件的 message 变量捕获了“属于”返回了被浏览器调用的单击处理函数的那一次渲染。所以当我点击“发送”时 message 被设置为那一刻在 input 中输入的内容。
但是如果我们想要读取并不属于这一次特定渲染的,最新的 props 和 state 呢?在函数式组件中,你也可以拥有一个在所有的组件渲染帧中共享的可变变量。它被成为“ref”:

1
2
3
4
5
6
7
8
9
10
11
12
function MessageThread() {
const [message, setMessage] = useState('');

// 保持追踪最新的值。
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});

const showMessage = () => {
alert('You said: ' + latestMessage.current);
};

在处理类似于 intervals 和订阅这样的命令式 API 时,ref 会十分便利

参考文章: