React 源码揭秘 | hooks原理

上篇我们说了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 副作用收集的过程,在后面说!

相关推荐
渣哥3 小时前
你以为 Bean 只是 new 出来?Spring BeanFactory 背后的秘密让人惊讶
javascript·后端·面试
烛阴3 小时前
为什么游戏开发者都爱 Lua?零基础快速上手指南
前端·lua
大猫会长3 小时前
tailwindcss出现could not determine executable to run
前端·tailwindcss
Moonbit3 小时前
MoonBit Pearls Vol.10:prettyprinter:使用函数组合解决结构化数据打印问题
前端·后端·程序员
533_3 小时前
[css] border 渐变
前端·css
云中雾丽4 小时前
flutter的dart语言和JavaScript的消息循环机制的异同
前端
地方地方4 小时前
Vue依赖注入:provide/inject 问题解析与最佳实践
前端·javascript·面试
云中雾丽4 小时前
dart的继承和消息循环机制
前端
世界哪有真情4 小时前
Trae 蓝屏问题
前端·后端·trae
Moment4 小时前
NestJS 在 2025 年:对于后端开发者仍然值得吗 😕😕😕
前端·后端·github