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

相关推荐
zhangxingchao23 分钟前
关于Android 构建流程解析的一些问题
前端
zheshiyangyang40 分钟前
Vue+ElementPlus的一些问题修复汇总
前端·javascript·vue.js
怣疯knight1 小时前
CryptoJS库中WordArray对象支持哪些输出格式?除了toString() 方法还有什么方法可以输出吗?WordArray对象的作用是什么?
前端·javascript
患得患失9491 小时前
【前端】【面试】【树】JavaScript 树形结构与列表结构的灵活转换:`listToTree` 与 `treeToList` 函数详解
开发语言·前端·javascript·tree·listtotree·treetolist
i建模1 小时前
Windows前端开发IDE选型全攻略
前端·ide·windows·node.js·编辑器·visual studio code
hamburgerDaddy12 小时前
从零开始用react + tailwindcs + express + mongodb实现一个聊天程序(三) 实现注册 登录接口
前端·javascript·mongodb·react.js·前端框架·express
用户51017613438682 小时前
Node.js接入DeepSeek实现流式对话
前端·后端
ClaNNEd@2 小时前
001第一个flutter文件
前端·flutter
希冀1232 小时前
【CSS】less基础(简单版)
前端·css·less
林涧泣3 小时前
【uniapp-Vue3】beforeRegister在注册用户入库前设置初始用户
前端·vue.js·uni-app