文章的 React 版本基于 v17.02 的 concurrent 模式
hooks 链表是在讲解所有 hook 前绕不开的话题,此文将以调试的角度出发分析 hooks 链表的结构与作用。
组件挂载时的链表逻辑#
首先我们试着对这段代码进行录制。
import { useLayoutEffect, useEffect, useState } from "react";
function UseEffectAnduseLayoutEffect() {
const [text, setText] = useState('wawawa');
useEffect(() => {
console.log('useEffect create 1');
// setText('useEffect create 1');
return () => {
console.log('useEffect destroy 1');
// setText('useEffect destroy 1');
}
}, []);
useLayoutEffect(() => {
console.log('useLayoutEffect create 1');
// setText('useLayoutEffect create 1');
return () => {
console.log('useLayoutEffect destroy 1');
// setText('useLayoutEffect destroy 1');
}
}, []);
return (
<div onClick={() => { setText('hahaha') }}>{ text }</div>
)
}
export default UseEffectAnduseLayoutEffect;
从结果中我们不难注意到 renderWithHooks 这一函数,该函数内执行了 UseEffectAnduseLayoutEffect,函数式组件作为函数被调用本身就是渲染的一环,除此之外在这个函数中也调用到了代码中的 hook。
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
...
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg); // 此处只需要关注调用UseEffectAnduseLayoutEffect的场景
...
return children;
}
在 React 内根据是否存在 current 来判断是挂载还是更新是一个比较常见的操作。由于我们目前记录的是初次挂载时的操作,因此此时 ReactCurrentDispatcher.current 为 HooksDispatcherOnMount 。至于为什么需要区分挂载 / 更新,后续会介绍。
接下来我们关注在 UseEffectAnduseLayoutEffect 内调用的 useState 与 useEffect 。我们将进入构筑 hooks 链表的相关逻辑。
在 UseEffectAnduseLayoutEffect 这一例子中最先被调用的是 useState ,而对其进行断点调试会进入 mountState 。
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,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
但是在其代码中我们目前只需要关注 mountWorkInProgressHook ,这是用来构建 hook 对象的
每调用一次 hooks 函数都会创建一个 hook 对象,其结构如下:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
}; // hook对象的结构 是构成 hooks链表的最小单元
if (workInProgressHook === null) {
// This is the first hook in the list
/**
* 这里可以看到,fiber 中的 memoizedState 是用来存储 hooks 链表的
*/
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
/**
* 后续在调用其他 hook 时,会直接接入到上一个 hook 对象的后面
*/
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
上述代码中请注意 workInProgressHook 这一个指针,它反映了当前组件中调用到了哪一个 hook 函数。
当代码中的 useState、useEffect、useLayout 全部执行完之后我们会获取到一条完整的 hooks 链表。
此时也可以看到 workInProgressHook 确实指向了最后一个 hook 对象
组件更新时的链表逻辑#
录制一下触发点击事件的记录,得到以下的结果。
此时 ReactCurrentDispatcher.current 为 HooksDispatcherOnUpdate。因此再次进入 UseEffectAnduseLayoutEffect 时调用的 hooks 将与挂载时不同。此时再次对 useState 打断点将进入 updateState 内的 updateWorkInProgressHook
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
// 根据注释 current hook list is the list that belongs to the current fiber 可知,current hook 指向了 current fiber 树上的 hooks 链表
// 毕竟更新操作时,必然已经存在了 current 树
// currentHook在函数组件调用完成时会被设置为null因此可以用来确认是否是刚开始重新渲染
if (currentHook === null) {
/**
* 根据注释 The work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook.
* currentlyRenderingFiber是一个 WIP 的 fiber
* 因此它的 alternate 是指向其对应的 current fiber 节点
*/
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// current节点存在,将nextCurrentHook指向current.memoizedState(hooks 链表)
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// 指针后移
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
代码冗长,指针又跳来跳去,看懂的最好办法还是自己调试一下。这里最重要的信息其实就是,在更新时会借 current 节点上的 hooks 链表去生成新的 hooks 链表。至于如何更新状态不在本文内介绍。
总结#
- 每次调用函数式组件都会生成一个 hooks 链表挂载在对应 fiber 的 memoizedState 上。