React:通俗易懂的 hooks 链表
前言
如今的 React 几乎是函数式组件 + hooks 的天下。我们知道,hooks 只能使用在函数式组件中,那么当组件挂载运行时,hooks 又是如何运行的呢?数据又是如何保存的呢?本篇文章将揭示 hooks 的原理。
函数组件与 hooks 的关系
renderWithHooks
在 React 源码中,当函数式组件初始化时,会调用 renderWithHooks
函数,我们来看看源码(这里只截取一部分与hooks有关的,感兴趣的朋友可以去看 React 源码)
//react-reconciler/src/ReactFiberHooks.js
export function renderWithHooks(
current, // currentFiber
workInProgress, // workInProgressFiber
Component, // 组件
props, // 组件 props
secondArg, //
nextRenderLanes,// Scheduler 优先级相关
) {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null; // 存储 hooks 的值
workInProgress.updateQueue = null; // 与 effect 副作用相关的更新队列
//...
// 区分是 初始化 还是 更新
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// ...
// 调用函数组件
let children = Component(props, secondArg);
// ...
return children
}
执行上面的代码时:
- 根据 current(currentFiber) 判断当前组件是否是初始化
- 是初始化,走
HooksDispatcherOnMount
- 是后续更新,走
HooksDispatcherOnUpdate
那上面的 HooksDispatcherOnMount、HooksDispatcherOnUpdate 又是个啥?
继续看源码:
//react-reconciler/src/ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {//mount 时
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
//...
};
const HooksDispatcherOnUpdate: Dispatcher = {//update 时
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
//...
};
// Hook 对象
export type Hook = {
memoizedState: any, // hook 对象的值,useState 保存 state , useEffect 保存 effect 链表,useRef 保存 { current: xxx } ....
baseState: any, // 初始 state
baseQueue: Update<any, any> | null, // 初始队列
queue: any, // 需要更新的队列,比如多次 setState
next: Hook | null, // 下一个 hook 节点
};
从上面的代码我们不妨大胆总结:在函数式组件挂载时,hooks 都会走 mountXXX,更新时,hooks 会走 updateXXX。
那这个结论有什么用呢?我们继续往下看。
hooks Mount阶段
mountWorkInProgressHook
当组件初始化的时候,会调用 mountXXX 的 hooks,而他们内部都会调用 mountWorkInProgressHook
的回调函数:
那这个 mountWorkInProgressHook 干了什么?看下面的代码:
也就是说,组件初始化时,按顺序调用 hooks 时,内部会通过 mountWorkInProgressHook() 函数创建 hook 节点,组装一个链表
,最终挂载到 Fiber 节点的 memoizedState
字段上。即:hook 链表会存储到 Fiber 的 memoizedState 属性上。
举个例子:
const App = () => {
const [state, setState] = useState(0);
const ref = useRef(1);
useEffect(() => {
setTimeOut(() => {
setState(2);
}, 500);
}, [])
}
我们依次使用了 useState、useRef、useEffect 三个 hooks,那么就会形成下面这种结构的链表:
- 调用 useState:
workInProgress.memoizedState: useState
^
workInProgressHook
- 调用 useRef:
workInProgress.memoizedState: useState -> useRef
^
workInProgressHook
- 调用 useEffect:
workInProgress.memoizedState: useState -> useRef -> useEffect
^
workInProgressHook
用图来描述的话就是:
总结: hook 每次执行时,都会通过 mountWorkInProgress 创建对应的 hook 节点
,将这些 hook 节点组成链表,赋值给 workInProgress。
注意: 不同的 hook,memoizedState 对应的值不同:(Fiber 上的 memoizedState 指向 hooks 链表,hook 身上的 memoizedState 存储他们自己的状态,二者不一样!
)
- useState:
memoizedState 等于 state 的值
- useEffect:
memoizedState 等于 effect 链表
,effect 链表又存储在 fiber.updateQueue 上,每个 effect 相当于 { create: callback, dep: dep } 这样的对象 - useRef:
memoizedState 等于 { current: initialValue }
- useMemo:
memoizedState 等于 [callback(), dep]
- useCallback:
memoizedState 等于 [callback, dep]
- useReducer:
memoizedState 等于state 的值
为什么不能在条件、循环里面使用 hook
在上述例子中,我们把 useRef
放入条件语句中
let ref = null;
let isFirst = true;
if (isFirst) {
curRef = useRef(1);
//初始化后将条件改为 false
isFirst = false;
}
后续组件重新 render 时,if 判断进不去,会发生下面的情况:
上面的图解看出:一旦在条件语句中声明hooks
,函数组件更新时,hooks
链表结构被破坏,currentFiber树
的 memoizedState
缓存 hooks链表
的信息,和 workInProgress
不一致,如果涉及到读取state
等操作,就会发生异常。因此不能在条件、循环语句中使用 hooks。
hooks Update阶段
前面我们说了,mount 的时候 hooks 调用 mountXXX,update 的时候调用 updateXXX。而在 update 阶段,会涉及到一个函数叫做 updateWorkInProgressHook
updateWorkInProgressHook
首先,来通读一遍源码:我们将源码分成 三个部分
。
// 首先,在组件中使用的多个 hook 会以链表的形式,存储在 fiber 节点的 memoizedState 字段上,
// currentHook 变量表示 currentFiber 树(当前渲染到页面)上的 旧的 hooks 链表。
// workInProgressHook 变量表示 一颗正在构建的新的 hooks 链表,它将会在构建成功后,
// 挂载到 workInProgressFiber 树上。
// currentlyRenderingFiber 变量就是 workInProgressFiber 树。
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
let currentlyRenderingFiber: Fiber = (null: any);
function updateWorkInProgressHook(): Hook {
/** 第一部分:
nextCurrentHook 中间变量,获取旧的 hooks 链表
**/
let nextCurrentHook: null | Hook;
// 如果 curentHook 为空
if (currentHook === null) {
// 通过 alternate 属性,找到对应的旧的 currentFiber 树
const current = currentlyRenderingFiber.alternate;
// 如果旧的 currentFiber 不为空,则取 memoizedState 拿到旧的 hooks 链表
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
// 否则为 null
nextCurrentHook = null;
}
} else {
// 如果 currentHook 不为空,则遍历下一个 hook 节点
nextCurrentHook = currentHook.next;
}
/** 第二部分:
nextWorkInProgressHook 中间变量,先复用一份旧的 hooks 链表形成新的 hooks 链表
**/
let nextWorkInProgressHook: null | Hook;
// 如果 workInProgressHook 为空,则表示还没指向 新的 hooks 地址。
if (workInProgressHook === null) {
// 这里同样通过 memoizedState 先拿到 currentFiber 的旧 hooks 链表。
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
// 不是头节点,遍历下一个 hook 节点
nextWorkInProgressHook = workInProgressHook.next;
}
/** 第三部分:
最终由 workInProgressHook 指向新的 hooks 链表
最终由 currentHook 指向旧的 hooks 链表
**/
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
currentHook = nextCurrentHook;
// 克隆旧的 hook 节点
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// 头节点,挂载到 workInProgressFiber 树上
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// 在链表后添加新节点
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
其实上面的代码可以概括为:
- 第一部分:由
nextCurrentHook
中间变量 记录旧的 hooks 链表 - 第二部分:由
nextWorkInProgressHook
中间变量 克隆旧的 hook 节点形成新的 hooks 链表。 - 第三部分:
currentHook
指向旧 hooks链表;workInProgressHook
指向新的 hooks 链表,返回workInProgressHook
。
之所以要克隆旧 hook,是为了能保留旧 hook 的状态,提供新旧 hook 对比的能力。
那其实上面的代码可以用下面的流程图解释:
克隆完毕后:
updateRef、updateMemo、updateCallback
这几个 hook 的代码比较简单,直接看代码
updateRef
updateRef 函数调用时,始终返回的是最开始的那个对象。因为 ref.current 的值在整个生命周期不变。
updateMemo
updateMemo 时,内部会通过 areHookInputsEqual(nextDeps, prevDeps)
判断依赖是否发生改变。没改变就返回 prevState[0], 改变了就重新执行回调函数 nextCreate()
,更新 hook 节点的 memoizedState,返回记忆值。
updateCallback
和 updateMemo 类似:
updateCallback 时,内部会通过 areHookInputsEqual(nextDeps, prevDeps)
判断依赖是否发生改变。没改变就返回 prevState[0], 改变了就 更新 hook 节点的 memoizedState
,返回记忆值函数。
updateState、updateReducer
因为 updateState 会调用 updateReducer
,所以放在一起来讲。 我们先来看 mountState 时做了什么
function mountState() {
// 创建 hook
const hook = mountWorkInProgressHook();
// baseState 保存初始值
hook.memoizedState = hook.baseState = initialState;
// 创建更新队列
const queue = {
pending, //要更新的队列
lanes, // 优先级
dispatch, // 也就是 setState()
lastRenderedReducer,
lastRenderedState: () => any,
};
// 绑定 dispatch
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
((currentlyRenderingFiber: any): Fiber),
queue,
)));
// 6. 返回 state 和 dispatch
return [hook.memoizedState, dispatch];
}
流程图如下:
那么当 updateReducer 时
// updateReducer 部分代码
function updateReducer() {
const queue = hook.queue;
let baseQueue = hook.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 如果 queue.pending 不为空,表示有尚未处理的更新
// 把它们合并到 baseQueue 中,是个单项环形链表
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
// 清理 queue.pending 待更新队列
queue.pending = null;
}
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = hook.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast= null;
let update = first;
do {
// 更新优先级相关
const updateLane = removeLanes(update.lane, OffscreenLane);
const isHiddenUpdate = updateLane !== update.lane;
const shouldSkipUpdate = isHiddenUpdate
? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
: !isSubsetOfLanes(renderLanes, updateLane);
if (shouldSkipUpdate) {
const clone = {
lane: updateLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
const revertLane = update.revertLane;
if (!enableAsyncActions || revertLane === NoLane) {
if (newBaseQueueLast !== null) {
const clone = {
lane: NoLane,
revertLane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
} else {
if (isSubsetOfLanes(renderLanes, revertLane)) {
update = update.next;
continue;
} else {
const clone = {
lane: NoLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
revertLane,
);
markSkippedUpdateLanes(revertLane);
}
}
const action = update.action;
if (shouldDoubleInvokeUserFnsInHooksDEV) {
reducer(newState, action);
}
if (update.hasEagerState) {
newState = ((update.eagerState: any): S);
} else {
// 计算得到新的 state
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 = queue.dispatch;
return [hook.memoizedState, dispatch];
}
上面的代码很复杂,但可以概括为:
将 pendingQueue 合并到 queue
根据优先级 计算最新的 state 并返回
updateEffect
同样,先看看 mountEffect 时干了个什么
function mountEffectImpl(
fiberFlags,
hookFlags,
create,
deps,
) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
// 调用了 pushEffect
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
createEffectInstance(),
nextDeps,
);
}
// pushEffect 部分代码
function pushEffect(
tag,
create,
inst,
deps,
) {
const effect: Effect = {
tag,
create,
inst,
deps,
next,
};
// 把创建的 effect 对象放到 fiber.updateQueue 上
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
//其余代码...
// lastEffect
componentUpdateQueue.lastEffect = effect.next = effect;
return effect;
}
mountEffect干了两件事:
创建 effect 对象
,记录了依赖数组、回调函数、清理函数、处理方式挂载到 fiber 树的 updateQueue 属性上
,updateQueue 是个链表,链表元素就是 effect 对象
如下图:
然后,updateEffect 时:
function updateEffectImpl(
fiberFlags,
hookFlags,
create,
deps,
) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const effect = hook.memoizedState;
const inst = effect.inst;
// currentHook is null when rerendering after a render phase state update.
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps,
);
}
updateEffect 的事情很简单:
- 通过
areHookInputsEqual
判断deps
是否相等 - 若相等,则不需要执行更新,effect tag 值是
NoHookEffect
- 若不相等,则更新 effect,effect tag 值为
hookEffectTag
effect 对象的其他属性:
- create 属性:useEffect 的第一个入参函数
- destroy 属性:
上一次 Effect 的 destroy
- deps 属性:依赖数组
那既然知道了 effect 对象哪些更新哪些不更新,那什么时候执行 effect 呢?
effect 的执行会在 Fiber 的 commit 阶段
,而入口是 commitRootImpl
函数,异步处理 effectList(是个单向循环链表)
,然后走 commitHookEffectListMount
函数
// commitHookEffectListMount 部分代码
function commitHookEffectListMount(flags, finishedWork) {
const updateQueue = finishedWork.updateQueue;
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
const inst = effect.inst;
const destroy = create();
inst.destroy = destroy;
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
上面的流程是:获取 updateQueue,从 lastEffect.next 指向的最后一个 effect, 开始循环处理
也就是说,在 render 阶段,useEffect 会把对应的 effect 放到 fiber.updateQueue 上,形成单项环形链表,然后在 commit 阶段,遍历 updateQueue,取出 effect 异步执行。
总结
mount 阶段时,hooks 走 mountXXX
,内部会调用 mountWorkInProgressHook
, 函数创建 hook 节点,组装一个链表
,最终挂载到 Fiber 节点的 memoizedState
字段上
update 阶段,hooks 走 updateXXX
,内部会调用 updateWorkInProgressHook
,克隆 hook 节点,然后拿到对应 hooks 的 hook 节点,然后根据每个 hook 内部具体的逻辑执行更新操作。
结语
以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论
转载自:https://juejin.cn/post/7231016295318585404