React 源码解读之 Hooks

说明:本文结论均基于 React 16.13.1 得出,若有出入请参考对应版本源码

题目

老规矩,在进入正题前,先做个题目:

下面的组件能按照期望工作吗(每隔一秒数字增加 1)?Why?

1
2
3
4
5
6
7
8
9
10
11
12
export default function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);

return <h1>{count}</h1>;
}

答案:不能,永远都渲染 1。

你做对了吗?

Hooks

Hook 是什么

可以理解为 Hook 就是一个对象,源码中定义如下:

1
2
3
4
5
6
7
export type Hook = {|
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
|};

Hooks 是怎么存储的

如下图所示,hooks 之间会形成一个链表并保持在 FiberNodememoizedState 属性之上,链表通过游标 currentHookworkInProgressHook 来进行遍历。我们知道 React 更新的时候会存在两棵 Fiber 树,一颗是上次已经构建好的 Fiber 树,一颗是正在构建的 Fiber 树,而 currentHookworkInProgressHook 分别是为这两棵树工作的。

怎么区分首次渲染和更新

首次渲染时,因为 hooks 链表还未形成,所以需要构建 hooks 链表,当调用 useState, useEffect 等函数时,最终都会调用 mountState, mountEffect 等方法。更新的时候,因为 hooks 链表已存在,需要对其进行更新,当调用 useState, useEffect 等函数时,最终都会调用 updateState, updateEffect 等方法:

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
const HooksDispatcherOnMount: Dispatcher = {
readContext,

useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useResponder: createDeprecatedResponderListener,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,
};

const HooksDispatcherOnUpdate: Dispatcher = {
readContext,

useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useResponder: createDeprecatedResponderListener,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,
};

React 通过当前正在构建的 workInProgress 所指向的 FibeNode 是否有对应的 current 来判断是否为首次渲染,从而调用不同的 hooks 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
...
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
...
export function useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>] {
const dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}

了解了 Hooks 的一些基本信息后,接下来看看常用的一些 Hooks 到底是怎么工作的吧。

常见 Hooks 分析

官网中列出了如下常用的 hooks:

Basic Hooks:

  • useState
  • useEffect
  • useContext

Additional Hooks:

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

这里我们暂时先不讨论 useContextuseDebugValue。而这些 hooks 中只需要搞清楚 useReduceruseEffect 即可,其他几个要么比较简单,要么跟 useReduceruseEffect 类似。

useReducer

我们以下面的例子来分析 useReducer 的运作流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default function App(props) {
const [state, dispatch] = useReducer(function myReducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + action.num};
case 'decrement':
return {count: state.count - action.num};
default:
throw new Error();
}
}, {count: 0})
return (
<div>
{state.count}
<button onClick={() => dispatch({type: 'decrement', num: 1})}>-</button>
<button onClick={() => {
dispatch({type: 'increment', num: 1})
dispatch({type: 'increment', num: 2})
}}>+</button>
</div>
)
}

首次渲染

首次渲染时,会执行 mountReducer:

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
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}

function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,

baseState: null,
baseQueue: null,
queue: null,

next: null,
};

if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}

这里调用 mountWorkInProgressHook 生成一个新的 hook,如果是第一个 hook,会挂载到 FibeNodememoizedState 上面,否则放到 hook 链表的末尾。最后,返回这个新的 hook。

接着会更新 hook 上面的属性,并返回 hook.memoizedStatedispatch(该方法后续再讨论)。总之,最后我们会得到如下的数据结构:

点击+号

当我们点击 + 号时,会执行两次 dispatch 方法:

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

function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
// 1
const update: Update<S, A> = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};

// Append the update to the end of the list.
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;

// 2
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
...
} else {
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
}
}
}

// 3
scheduleUpdateOnFiber(fiber, expirationTime);
}
}

我们去掉一些暂时不关心的代码,整个代码就分成了三个部分:

  1. 创建一个新的更新,将所有的更新形成一个环状链表并挂载到 queue.pending 上面
  2. 计算出 eagerReducereagerState
  3. FiberNode 上调度更新(暂时不关心,只需要知道它会开启一个宏任务,在下一个事件循环对组件进行更新)

经过两次 dispatch 后,我们的数据结构会变成如下这样:

更新时

下一个事件循环时,React 会进行更新,当执行到 useReducer 时会执行 updateReducer

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
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;

queue.lastRenderedReducer = reducer;

const current: Hook = (currentHook: any);

// 得到更新链表的入口
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}

if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;

let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
// 遍历更新链表,对 state 进行更新
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
...
} else {
...
// Process this update.
if (update.eagerReducer === reducer) {
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);

if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}

if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}

hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;

queue.lastRenderedState = newState;
}

const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}

这里首先调用了 updateWorkInProgressHook,即通过当前 FiberNode 上的 Hook 来生成新的 FiberNode 上新的 Hook。剩下的代码较长,但其核心思想就是遍历更新链表中的 action 对 state 进行更新,最终数据结构会变成如下所示:

注意到我们新生成的 FiberNode 中的 hook 的 state 已经更新成了 { count: 3 }

useState

搞清楚了 useReduceruseState 就比较简单了,对比一下代码就可以发现,两者是很类似的。

首次渲染时,主要不同之处在于 mountState 中的 lastRenderedReducer 用的是内置的 basicStateReducer

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
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}

function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
/**
不同之处,useState 使用的是内置的 reducer
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
*/
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}

更新时,updateState 内部用的就是 updateReducer

1
2
3
4
5
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}

useEffect

我们通过下面这个例子来分析 useEffect

1
2
3
4
5
6
7
8
9
10
export default function App(props) {
const [num, setNum] = useState(0)
useEffect(() => {
console.log(1)
}, [])
useEffect(() => {
console.log(2, num)
}, [num])
return <div onClick={() => setNum(num+1)}>{num}</div>
}

首次渲染

首次渲染会执行 mountEffect

1
2
3
4
5
6
7
8
9
10
11
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}

实际上执行的是 mountEffectImpl

1
2
3
4
5
6
7
8
9
10
11
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.effectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create,
undefined,
nextDeps,
);
}

这里首先也是生成了一个 hook,然后对当前的 FiberNodeeffectTag 进行标记,表示这个节点是有 useEffect 的副作用的。最后会调用 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
26
27
function pushEffect(tag, create, destroy, deps) {
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;
}

该方法会返回一个 Effect 类型的对象挂载到 hookmemoizedState 属性之上。同时,通过 FiberNode 上的 updateQueue.lastEffect 会将所有 Effect 串起来。具体到我们的例子,首次渲染后,会形成如下的结构:

在 React 渲染流程的 commit 阶段会遍历这些 updateQueue.lastEffect 下的所有 Effect 进行处理(详见React 源码解读之一首次渲染流程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
commitHookEffectListMount(
HookPassive | HookHasEffect, // 值为 5
finishedWork,
);
...

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}

点击 div 后

点击 div 后,产生一个更新:

更新时

更新时,会执行 updateEffect

1
2
3
4
5
6
7
8
9
10
11
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(
UpdateEffect | PassiveEffect, // 值为 4
HookPassive,
create,
deps,
);
}

最终执行的是 updateEffectImpl:

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
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;

if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookEffectTag, create, destroy, nextDeps);
return;
}
}
}

currentlyRenderingFiber.effectTag |= fiberEffectTag;

hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag, // 值为 5
create,
destroy,
nextDeps,
);
}

如果这次更新时的 deps 没有发生变化,则推到 updateQueue.lastEffect 里面的 Effecttag 将会是 4,否则为 5。所以,两次 useEffect 执行完后,我们的结构变成如下所示:

这样,在 React 渲染流程的 commit 阶段执行副作用时就不会执行 tag 为 4 的 Effect 了:

1
2
3
4
commitHookEffectListMount(
HookPassive | HookHasEffect, // 值为 5
finishedWork,
);

useLayoutEffect

搞清楚了 useEffectuseLayoutEffect 就比较简单了,在首次渲染和更新时也是分别调用 mountEffectImplupdateEffectImpl,仅传参不同而已:

1
2
3
4
5
6
7
8
9
10
11
12
13
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(UpdateEffect, HookLayout, create, deps);
}

function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

两种最主要的不同之处在于执行的时机,详见React 源码解读之一首次渲染流程

useRef

useRef 非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
if (__DEV__) {
Object.seal(ref);
}
hook.memoizedState = ref;
return ref;
}

function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}

useState 的不同之处在于:

  1. useRef 常用来记录与当前 FiberNode 相关的数据,且这些数据的变更不触发渲染
  2. Ref 值的修改是“即时”的

useCallback

useCallback 也很简单。首次渲染时将传入的函数和依赖 [callback, nextDeps] 保存在 HookmemoizedState 之上。更新时判断依赖是否发生变化,如果变化了,则返回新传入的函数,否则返回上次保存的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}

useMemo

useMemouseCallback 类似,只不过 useMemo 会把传入的函数执行后返回其结果:

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
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}

function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}

useImperativeHandle

这个函数可能很多人没有用到过,不过它很强大。比如,现在我写了一个函数型的 Form 组件:

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
class FormStore {
// 提交表单
submit = () => {}
// 验证表单
validate = () => {}
}

function useForm() {
const formRef = useRef()
if (!formRef.current) {
formRef.current = new FormStore()
}
}

const Form = forwardRef((props, ref) => {
const formInst = useForm()
// TODO 怎么把 formInst 传递给使用者?
return <form onSubmit={formInst.submit} />
})

// 使用
function App() {
const form = useRef()
useEffect(() => {
// 一秒后自动提交表单
setTimeout(() => {
form.submit()
}, 1000)
}, [])

return (
<div>
<Form ref={form} />
</div>
)
}

现在的问题是我该如何让使用者拿到我组件内的 formInst 这个对象能?如果是类组件的话比较好办,我们把 formInst 挂载到类组件的实例上面,外部通过 ref 拿到的本身就是实例了。如果是函数组件的话,就轮到 useImperativeHandle 上场了:

1
2
3
4
5
const Form = forwardRef((props, ref) => {
const formInst = useForm()
useImperativeHandle(ref, () => formInst)
return <form onSubmit={formInst.submit} />
})

更多可以参考 rc-field-form

那这个神奇的方法到底是怎么实现的呢?我们来看看:

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
function mountImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;

return mountEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}

function updateImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;

return updateEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}

原来他就是一种特殊的 useLayoutEffect,所以我们只需要搞清楚 imperativeHandleEffect 做了什么就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function imperativeHandleEffect<T>(
create: () => T,
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
) {
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create();
refCallback(inst);
return () => {
refCallback(null);
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
const inst = create();
refObject.current = inst;
return () => {
refObject.current = null;
};
}
}

这里无非就是把传入 useImperativeHandle 的第二个参数执行后的结果作为 ref 的值而已。

题目解答

现在,让我们来看看文章开头的题目:

下面的组件能按照期望工作吗(每隔一秒数字增加 1)?Why?

1
2
3
4
5
6
7
8
9
10
11
12
export default function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);

return <h1>{count}</h1>;
}

我们还是通过数据结构来一步步分析这个问题吧:

首次渲染时,会形成如下数据结构:

当我们执行 useEffectcreate 方法时,会生成一个定时器,该定时器中访问了父函数的变量 count,形成闭包,count 值为 0。

1 秒后,定时器执行,此时会产生一个更新,且 action 为 1:

之后会进入更新的流程,生成新的 FiberNode

因为 useEffect 的依赖没有变化,所以它的 create 不会执行,不会产生新的定时器。

1 秒后,首次渲染生成的定时器又会产生一个更新,且 action 仍然为 1,之后的过程就是一样的了。

所以页面会一直显示 1。

产生这个 Bug 的原因是,我们对 useEffect 撒谎了,既然 useEffect 里面依赖了 count,那依赖数组里面就必须写上:

1
2
3
4
5
6
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);

这样,每次更新的时候,我们都会执行 useEffect 对应的 Hook 中的 destroy 方法取消上一次的定时器,然后执行 create 产生新的定时器,所以就能得到我们需要的结果了。

当然,还有一种做法,就是 setCount 的时候,使用回调函数的方式:

1
2
3
4
5
6
useEffect(() => {
const id = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => clearInterval(id);
}, []);

这里就不再分析了。

总结

本文阐述了 React 中 hooks 的结构及存储模型,并结合源码介绍了常用的一些 hooks,最后对一个经典的题目进行了分析。