上篇我们说了updateQueue的实现原理,这篇我们说一下hooks,
fiberHooks实现可以在react-reconciler/fiberHooks.ts 找到。
老生常谈的问题,为什么hooks有顺序,hook函数怎么知道你在哪运行的hooks? 下面我们逐一讨论。
从入口开始
回忆一下,BeginWork.ts 会根据Fiber对象的tag属性,分配处理方法,其中,对于函数组件,会调用UpdateFunctionComponent 其实现如下:
TypeScript
/** 处理函数节点的比较 */
function updateFunctionComponent(
wip: FiberNode,
Component: Function,
renderLane: Lane
): FiberNode {
const nextChildElement = renderWithHooks(wip, Component, renderLane);
reconcileChildren(wip, nextChildElement);
return wip.child;
}
renderWithHooks就是用来运行函数体本身的, 其中也就包含了hooks的处理逻辑
一些全局变量
hooks的实现逻辑可以在 react-reconciler/fiberHook.ts中找到,其中包含了一些全局变量
- currentRenderingFiber 当前正在渲染处理的Fiber对象
- workInProgressHook 当前正在处理的hook
- currentHook 当前正在处理hook对应的alternate FIber对象的hook (为了从currentHook获取属性)
- renderLane 当前update对应的优先级lane
Hook数据结构
hook数据结构ts定义如下:
TypeScript
/** 定义Hook类型 */
export interface Hook {
memorizedState: any;
updateQueue: UpdateQueue<any>;
next: Hook | null;
}
其中,包含当前hook的状态 memorizedState 这个对于不同的hook 作用是不同的,有的需要存储状态有的不需要,不需要存储状态的hook这个字段就是null
比如对于useState hook 其memorizedState对应的就是当前state的值
对于useCallback或者useMemo 其memorizedState对应的就是当前缓存的函数或者Memo值 等等
updateQueue就是当前hooks对应的更新队列,比如在useState中,就会吧每次调用setter的更新加入到这个队列中,但是在useMemo useCallback中 这个字段就不被使用
next指向下一个hook 也就是说 一个函数内的hook顺序,需要保持稳定,每次运行都会根据alternate Fiber重新创建新的hooks链,如果顺序变化会导致hook返回结果错乱
一个Fiber上的所有hooks都保存在其memorizedState上,和updateQueue不同的是,其是一个单项链表,不是环
TypeScript
function FnComp(){
useState(100)
useMemo(()=>{},[])
useEffect(()=>{}),[]
return <>...</>
}
如上函数组件,对应的hook结构如下:

renderWithHooks
renderWithHooks是运行函数组件,处理hooks的入口,其中初始化了函数运行的上下文,实现如下:
TypeScript
/** 运行函数组件以及hooks */
export function renderWithHooks(
wip: FiberNode,
Component: Function,
lane: Lane
) {
// 主要作用是,运行函数组件 并且在函数运行上下文挂载currentDispatcher 在运行之后 卸载Dispatcher
// 保证hook只能在函数组件内运行
// 设置当前正在渲染的fiber
currentRenderingFiber = wip;
// 清空memoizedState
wip.memorizedState = null;
// 重置 effect链表
wip.updateQueue = null;
// 当前已经渲染的fiber
const current = wip.alternate;
renderLane = lane;
if (current !== null) {
// update
currentDispatcher.current = {
useState: updateState,
useEffect: updateEffect,
useTransition: updateTransition,
useRef: updateRef,
useMemo: updateMemo,
useCallback: updateCallback,
};
} else {
// mount
currentDispatcher.current = {
useState: mountState,
useEffect: mountEffect,
useTransition: mountTransition,
useRef: mountRef,
useMemo: mountMemo,
useCallback: mountCallback,
};
}
// 运行函数
const pendingProps = wip.pendingProps;
const childrenElements = Component(pendingProps);
// 恢复
currentRenderingFiber = null;
workInProgressHook = null;
currentHook = null;
currentDispatcher.current = null;
renderLane = NoLane;
return childrenElements;
}
其中可以看到,首先对currentRenderingFiber进行了初始化,设置为当前正在渲染的Fiber,这也就是为什么 hook知道自己在哪里被调用,就是在运行hook函数之前,currentRenderingFIber已经被设置了,hook函数内读取这个全局变量,就能获取当前正在处理的Fiber,在函数组件运行完之后,currentRenderingFiber会被恢复为null,任何在函数组件外调用的hook函数,由于没有这个全局变量,都会报 "无法在函数组件外部使用hooks"
同时 会重置当前wip fiber的memorizedState和updateQueue 因为这些对象可能是旧的,在函数执行渲染的时候,会重新设置这些值。
renderLane也会通过传入的wipRenderedLane进行设置,在全局就可以直接获取当前正在渲染的lane
dispatcher
dispatch是一个共享的对象,其被定义在react/currentDispatcher.ts 下,其中包含了React支持的所有hooks类型 ts定义如下
TypeScript
export interface Dispatcher {
useState: <T>(initialState: T | (() => T)) => [T, Dispatch<T>];
useEffect: (create: EffectCallback, deps: HookDeps) => EffectCallback | void;
useTransition: () => [boolean, (callback: () => void) => void];
useRef: <T>(initialValue: T) => { current: T };
useMemo: <T>(nextCreate: () => T, deps: HookDeps) => T;
useCallback: <T>(callback: T, deps: HookDeps) => T;
}
在currentDispatcher中 包含了一个共享的currentDisptacher对象,这个对象包含一个current属性,初始化为null, 同时包含一个resolveDispatcher函数,用来解析Dispatcher
TypeScript
/** 共享的 当前的Dispatcher */
export const currentDispatcher: { current: Dispatcher | null } = {
current: null,
};
/** 解析 返回dispatcher */
export function resolveDispatcher(): Dispatcher {
const dispatcher = currentDispatcher.current;
if (dispatcher === null) {
throw new Error("无法在函数组件外部使用hooks");
}
return dispatcher;
}
在renderWithHook中,调用组件函数之前,会先引入这个currentDispatcher对象并且给其current属性设置hook函数
其会根据当前的Fiber是挂载状态mount 还是更新状态 update 来挂载不同的hook函数,是否为挂载还是更新由wip.alternate是否存在决定
TypeScript
// renderWithHook
const current = wip.alternate;
if (current !== null) {
// update 挂载更新hook
currentDispatcher.current = {
useState: updateState,
useEffect: updateEffect,
useTransition: updateTransition,
useRef: updateRef,
useMemo: updateMemo,
useCallback: updateCallback,
};
} else {
// mount 挂载mounthook
currentDispatcher.current = {
useState: mountState,
useEffect: mountEffect,
useTransition: mountTransition,
useRef: mountRef,
useMemo: mountMemo,
useCallback: mountCallback,
};
}
在 react/index.ts中,暴露了所有hooks对应的接口, 部分如下:
TypeScript
export function useState<State>(initialState: (() => State) | State) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
export function useEffect(create: EffectCallback, deps: HookDeps) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, deps);
}
export function useTransition() {
const dispatcher = resolveDispatcher();
return dispatcher.useTransition();
}
export function useRef<T>(initialValue: T) {
const dispatcher = resolveDispatcher();
return dispatcher.useRef<T>(initialValue);
}
... ... ...
其本质就是调用了resolveDispatcher函数获取当前对应的dispatcher对象,并且根据不同的hooks类型调用不同的处理函数。
当你在函数组件外部调用hook函数时,由于没有调用renderWithHook 所以没有给currentDispatcher.current赋dispatcher对象,所以在resolveDispatcher时,发现当前current属性为null,则报错 "无法在函数组件外部使用hooks"
在renderWithHook的组件函数运行完成之后,会重新把 currentDispatcher.current = null 同时也会把renderLane currentRenderingFiber 以及workInProgressHook currentHook都置空。并且返回child Element。
用这种方式 React就能识别到底在哪个Fiber中调用的hooks 以及判断是否在函数组件外部调用了hook
mount&update
对于挂载和更新阶段,虽然都是运行useXXX hook函数,但是由于通过wip.alternate判断挂载了不同的处理函数,其内部实现是不一样的,比如useState在挂载阶段对应的就是mountState处理函数,在更新阶段调用的就是updateState。
我们需要实现两个函数,获取当前的hook
我们上面说了 一个函数组件的Fiber的hook,以一个链表的形式挂载fiber对象的memorizedState上,我们就需要一个逻辑,能够返回每次待执行的hook对象
mountWorkInProgressHook
mountWorkInProgressHook 对应挂载阶段创建hooks链的过程,其实现如下
TypeScript
/** 挂载当前的workInProgressHook 并且返回 */
function mountWorkInProgressHook() {
if (!currentRenderingFiber) {
throw new Error("hooks必须在函数组件内部调用!");
}
const hook: Hook = {
memorizedState: null,
updateQueue: new UpdateQueue(),
next: null,
};
// hook的挂载方式是 currentRenderdingFiber.memorizedState -> hook1 -next-> hook2 -next-> hook3 -next-> null
if (currentRenderingFiber.memorizedState === null) {
// 第一次挂载
currentRenderingFiber.memorizedState = hook;
} else {
// 非第一次挂载
workInProgressHook.next = hook;
}
// 设置workInProgressHook
workInProgressHook = hook;
return hook;
}
在初始化阶段,此时memorizedState为null,我们需要逐个创建hook对象
每次调用,意味着要创建一个hook对象,检查:
如果currentRenderingFiber的memorizedState如果为空,说明当前一个hook都没有,此时为函数组件的第一个hook,那么创建hook对象并赋在memorizedState属性和全局的workInProgressHoo
k上, 并且返回。
如果currentRenderingFiber.memorizedState不为空,那么说明前面已经有hook对象挂上去了,此时前面的对象是workInPrigressHook,那么在其next上挂上当前创建的hook对象即可,并且修改wipHook返回。
updateWorkInProgressHook
次函数主要作用在更新阶段,在更新阶段时,由于renderWithHook中已经将当前渲染Fiber的memorizedState置为null了。那么意味着hooks队列需要重新创建,但是此时的创建需要根据wip.alternate的Fiber对象来复用。实现如下:
TypeScript
/** 根据current 挂载当前的workInProgressHook 并且返回 */
function updateWorkInProgressHook() {
if (!currentRenderingFiber) {
throw new Error("hooks必须在函数组件内部调用!");
}
// 找到当前已经渲染的fiber -> current
const current = currentRenderingFiber.alternate;
// currentHook是指向current元素的hook指针
if (currentHook === null) {
// 当前还没有currentHook 第一个元素
if (current) {
currentHook = current.memorizedState;
} else {
currentHook = null;
}
} else {
// 如果有currentHook 说明不是第一个hook
currentHook = currentHook.next;
}
// 如果没找到currentHook 说明hook数量对不上
if (currentHook === null) {
throw new Error("render more hooks than previouse render!");
}
// 拿到currentHook了 需要根据其构建当前的workInProgrerssHook
const hook: Hook = {
memorizedState: currentHook.memorizedState,
updateQueue: currentHook.updateQueue,
next: null,
};
if (currentRenderingFiber.memorizedState === null) {
currentRenderingFiber.memorizedState = hook;
} else {
workInProgressHook.next = hook;
}
workInProgressHook = hook;
return hook;
}
和mount阶段类似,多了一个在currentRenderingFiber.alternate.memorizedState上找currentHook的过程,并且根据找到的旧的currentHook,给当前创建的新的Hook对象的updateQueue memorizedState赋值,并挂载新的hook到当前wip.memorizedState链上!
需要注意的是,如果在更新阶段没找到currentHook,那么说明此次更新一定比上次更新渲染了更多的hook函数,此时就会因为hooks无法对应报错!
说完了最基础创建复用hook的逻辑,我们就逐个看一下主要hooks的原理
useState
useState用来在函数组件内记录状态,虽然函数组件是纯函数,但是每次运行到hook函数时会返回上次记录的值。
这是代数效应的思想,通过引入useState这个代数符号把副作用拿到函数外部。
mountState
首先看mountState的逻辑
TypeScript
/** 挂载state */
function mountState<T>(initialState): [T, Dispatch<T>] {
const hook = mountWorkInProgressHook();
let memorizedState: T;
// 计算初始值
if (typeof initialState === "function") {
memorizedState = initialState();
} else {
memorizedState = initialState;
}
// 挂载memorizedState到hook 注意别挂载错了 currentRenderingFiber 也有一样的memorizedState
hook.memorizedState = memorizedState;
// 设置hook.taskQueue.dispatch 并且返回,注意dispatch是可以拿到函数组件外部使用的,所以这里需要绑定当前渲染fiber和updateQueue
hook.updateQueue.dispatch = dispatchSetState.bind(
null,
currentRenderingFiber,
hook.updateQueue
);
hook.updateQueue.baseState = memorizedState;
// 保存上一次的值
hook.updateQueue.lastRenderedState = memorizedState;
return [memorizedState, hook.updateQueue.dispatch];
}
挂载阶段的逻辑很简单,就是
首先获取当前的hook对象,根据传入的initialState的类型获取初始值,这个和action的处理逻辑类似!
把初始值,保存在当前hook对象的memorizedState中
这里你需要区分Fiber对象的memorizedState和Hook对象的memorizedState
给UpdateQueue的baseState赋初始值
给updateQueue的lastRenderdState赋初始值,这个逻辑在eagerState中用到 先忽略
给dispatchSetState函数绑定当前渲染的Fiber对象和当前hook的updateQueue,为什么要绑定,因为dispatchSetState就对应useState的第二个数组项setter,setter可以在任意位置调用,所以要保存当前的Fiber和updateQueue信息。
把绑定的函数赋给updateQueue.dispatch属性,作为更新队列的更新器
最后将memorizedState和hook.updateQueue.dispatch封装成数组返回
dispatchSetState
这个函数用来派发更新,前面也讲过,其逻辑如下:
TypeScript
/** 派发修改state */
function dispatchSetState<State>(
fiber: FiberNode,
updateQueue: UpdateQueue<State>,
action: Action<State>
) {
// 获取一个优先级 根据 dispatchSetState 执行所在的上下文
const lane = requestUpdateLane();
// 创建一个update对象
const update = new Update(action, lane);
// 入队 并且加入到fiber上
updateQueue.enqueue(update, fiber, lane);
// 开启调度时,也需要传入当前优先级
scheduleUpdateOnFiber(fiber, lane);
}
其本质就是获取当前上下文的优先级Lane,创建更新,推入updateQueue,并且开启调度。
updateState
updateState作用于更新阶段,其逻辑如下:
TypeScript
function updateState<T>(): [T, Dispatch<T>] {
const hook = updateWorkInProgressHook();
const { memorizedState } = hook.updateQueue.process(
renderLane,
(update) => {
currentRenderingFiber.lanes = mergeLane(
currentRenderingFiber.lanes,
update.lane
);
}
);
hook.memorizedState = memorizedState;
hook.updateQueue.lastRenderedState = memorizedState;
return [memorizedState, hook.updateQueue.dispatch];
}
其实现也很简单,就是调用当前hook的updateQueue中的process方法,处理更新,返回新的状态值
更新当前hook的memorizedState以及updateQueue.lastRenderState 返回状态和dispatch方法。
useMemo & useCallback
这两个hook主要用于缓存变量和函数,大多情况下需要配合React.memo使用,其实现也很简单
mountMemo
挂载情况下,useMemo传入一个Create函数,一个deps数组,mount状态下的逻辑就是调用Create函数,把返回值和数组deps存入hook.memorizedState 如下
TypeScript
function mountMemo<T>(nextCreate, deps) {
const hook = mountWorkInProgressHook();
hook.memorizedState = [nextCreate(), deps];
return hook.memorizedState[0];
}
updateMemo
updateMemo需要判断,两次传入的deps是否相等,如果相等则还返回上次保存的值,如果不等就重新运行Create函数,并且返回
TypeScript
function updateMemo<T>(nextCreate, deps) {
const hook = updateWorkInProgressHook();
const [prevValue, prevDeps] = hook.memorizedState;
if (areHookInputsEqual(prevDeps, deps)) {
hook.memorizedState = [prevValue, deps];
} else {
hook.memorizedState = [nextCreate(), deps];
}
return hook.memorizedState[0];
}
其中,deps为数组,比较两个数组是否相等,使用areHookInputsEqual
areHookInputsEuqal 判读两个deps数组相等
areHookInputsEuqal用来判断数组每个对应位置上的值是否相等,其判断的方式不是 == 或者 === 而是Object.is 对于NaN +0 -0 有不同的处理
TypeScript
/** 潜比较Deps */
function areHookInputsEqual(prevDeps: HookDeps, curDeps: HookDeps) {
if (prevDeps === null || curDeps === null) return false;
if (prevDeps?.length !== curDeps?.length) return false;
for (let i = 0; i < prevDeps.length; i++) {
if (Object.is(prevDeps[i], curDeps[i])) {
continue;
}
return false;
}
return true;
}
mountCallback & updateCallback
对于useCallback 和 useMemo一样,只不过由于其缓存的是函数,对于传入的callback不运行,直接返回,如下:
TypeScript
/** useCallback */
function mountCallback<T>(callback, deps) {
const hook = mountWorkInProgressHook();
hook.memorizedState = [callback, deps];
return hook.memorizedState[0];
}
function updateCallback<T>(callback, deps) {
const hook = updateWorkInProgressHook();
const [prevCallback, prevDeps] = hook.memorizedState;
if (areHookInputsEqual(prevDeps, deps)) {
hook.memorizedState = [prevCallback, deps];
} else {
hook.memorizedState = [callback, deps];
}
return hook.memorizedState[0];
}
useRef
useRef本质上就是保存一个值,这个值可以用作ref处理,也可以用来保存一些你希望保存但是不希望触发更新的值,如下:
TypeScript
/** 挂载Ref */
function mountRef<T>(initialValue: T): { current: T } {
const hook = mountWorkInProgressHook();
hook.memorizedState = { current: initialValue };
return hook.memorizedState;
}
/** 更新Ref 其实就是保存一个值 */
function updateRef<T>(): { current: T } {
const hook = updateWorkInProgressHook();
return hook.memorizedState;
}
useTransition
useTransition是React@18 引入的hooks 其返回一个[isPending, startTransition]
其中,startTranstion传入一个callback,callback内创建的更新会被赋低优先级 即 TranstionLane
在需要渲染一些大规模的更新时,建议将其放在startTranstion中,这样如果在渲染过程中触发用户事件,由于用户事件优先级更高,会中断render过程去处理用户事件对应的更新,达到页面不卡死的效果!
mountTranstion
mount阶段代码如下:
TypeScript
/** transition */
function mountTransition() {
// 设置pending state
const [isPending, setPending] = mountState<boolean>(false);
// 获得hook
const hook = mountWorkInProgressHook();
// 创建startTransition
const start = startTransition.bind(null, setPending);
// 记录start
hook.memorizedState = start;
// 返回pending和start
return [isPending, start] as [boolean, (callback: () => void) => void];
}
mount阶段,首先需要在内部设置一个isPendingHooks ,相当于帮用户创建了一个isPending hook
获取当前hook 并且把startTranstion绑定上isPending的setter 存入memorizedState 并且和isPending一起返回
updateTranstion
updateTranstion很简单,只需要将上次更新存储的start函数,以及本次更新计算出的isPending返回即可!
TypeScript
function updateTransition() {
const [isPending] = updateState<boolean>();
const hook = updateWorkInProgressHook();
const start = hook.memorizedState;
return [isPending, start] as [boolean, (callback: () => void) => void];
}
下面我们着重看一下startTranstion函数
startTranstion
startTranstion的逻辑如下
TypeScript
function startTransition(setPending: Dispatch<boolean>, callback: () => void) {
// 开始transition 第一次更新 此时优先级高
setPending(true);
// transition过程,下面的优先级低
const prevTransition = isTransition;
// 设置标记 表示处于transition过程中,在fiberHook.ts/requestUpdateLane会判断这个变量,如果true则返回transtionLane
isTransition = true;
// 设置标记 (在react原版中 这里是 1)
// 第二次更新 优先级低
callback();
// 第三次更新 重新设置pending 优先级低
setPending(false);
// 恢复isTransition
isTransition = prevTransition;
}
在FiberHooks中 导出了全局变量isTranstion
TypeScript
// 导出共享变量
export let isTransition = false;
当startTransition运行的过程中,会先把isPending更新为true,此次更新对应的Update和调用startTranstion之前外部上下文的优先级一致,比如是SyncLane,是一个比较高的优先级
设置之后,会把全局的isTranstion设置为true 并且运行callback
callback中,如果触发更新,就一定要通过requestUpdateLane获取当前上下文的优先级,在其中
TypeScript
export function requestUpdateLane(): Lane {
if (isTransition) {
return TransitionLane
}
const currentUpdateLane = schedulerPriorityToLane(
scheduler.getCurrentPriorityLevel()
);
return currentUpdateLane;
}
会检查,如果当前isTranstion变量为true,就会直接返回一个低优先级的TranstionLane
此时 callback内部的更新就是低优先级了
同时,startTranstion也会在isTranstion=true的范围内,设置isPending为false,这个更新也是低优先级的,这样就保证了在低优先级更新时,isPending才会变成false
最后恢复现场,把isTranstion改成之前的值,结束运行
用这样的方式,实现了过渡效果
useDeferedValue
和useTranstion同理,本质上也是给一个低优先级的DeferedLane,只有当renderLane处理到这个DeferedLane时,才更新数据,代码如下,不过多赘述。
TypeScript
function updateDeferedValue<T>(value: T) {
const hook = updateWorkInProgressHook();
const prevValue = hook.memorizedState;
// 相同 没变化,直接返回
if (Object.is(value, prevValue)) return value;
if (isSubsetOfLanes(renderLane, DeferredLane)) {
// 低优先级DeferedLane时
hook.memorizedState = value;
return value;
} else {
// 优先级高于Deferedlane时
currentRenderingFiber.lanes |= DeferredLane
scheduleUpdateOnFiber(currentRenderingFiber, DeferredLane);
return prevValue;
}
}
function mountDeferedValue<T>(value: T) {
const hook = mountWorkInProgressHook();
hook.memorizedState = value;
return hook.memorizedState;
}
useEffect 由于要涉及到commit 副作用收集的过程,在后面说!