React useState源码深度解析:Fiber、Hook链表与调度机制

在使用React开发一段时间后,经常接触到Hooks的使用,对其中的一些Hooks设计比较感兴趣,所以这篇文章围绕使用场景最为频繁的useState来进行拆解分析。

场景分析

以下代码是我们在开发场景中经常会遇到的,通过引入useStateHook函数来实现函数组件内的状态管理,如果你了解其Fiber架构的话,就会知道在组件挂载阶段会创建该App组件对应的Fiber下的Hook对象,其绑定在Fiber节点的memoizedState属性上,其存储了该Fiber下对应的Hook对象链表,通过遍历该链表可以获得对应的Hook对象,所以我们当前组件下也是按顺序建立链表连接。

js 复制代码
import { useState } from 'react'

function App() {
    const [count, setCount] = useState(0);
    
    const add = () => {
        setCount(count + 1)
    }
    
    return (
        <div>
            <h1>useState Hook底层原理分析</h1>
            <button onClick={add}>+1</button>
        </div>
    )
}

底层原理

在底层实现中,其函数定义如下:

js 复制代码
export function useState(initialState) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

首先通过调用resolveDispatcher函数返回我们的全局对象ReactCurrentDispatcher.current,该对象在renderWithHooks函数(在render阶段被beginWork函数调用)内进行赋值,如下代码,这里的current对象为UI中正在渲染的Fiber,本文称为旧Fiber 。如果旧Fiber不存在(首次渲染),或者旧Fiber的memoizedState为空(首次渲染该组件),则将dispatcher设为挂载阶段对象HooksDispatcherOnMount

注意,属性memoizedState保存该Fiber内存储的Hooks链表,这里和Hook对象的memoizedState不一样,Hook对象的memoizedState表示上一次更新后的最终状态值。

js 复制代码
ReactCurrentDispatcher.current =
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

接着调用不同阶段下该dispatcher下的Hooks方法。其内部代码如下所示:

js 复制代码
// 挂载阶段不同Hook使用的方法
const HooksDispatcherOnMount = {
  ...
  useState: mountState,
};

// 更新阶段不同Hook使用的方法
const HooksDispatcherOnUpdate = {
  ...
  useState: updateState,
};

由于我们只考虑useState的内部实现,所以这里来看mountStateupdateState这两个方法。

挂载阶段

在挂载阶段,从创建Hook对象到创建状态更新队列queue,通过bind函数预置参数返回一个新的函数,最终返回一个数组,也就是我们平常开发中结构后的statesetState,分别对应本次更新的状态值和更新函数。

其中包括了对Hook链表的处理,通过遍历Hook链表获取其更新前后的状态值,从而在使用React开发时其能够实现状态变化触发重渲染。

js 复制代码
function mountState(initialState) {
  // 返回新创建的Hook对象
  const hook = mountWorkInProgressHook();
  // 因为是挂载阶段,传参可能为函数
  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  // 其中baseState是因优先级问题被跳过的更新时,下一次计算的基础状态起点
  hook.memoizedState = hook.baseState = initialState;
  // 存储在Hook的队列对象中,其作为状态管理机制的基础
  const queue = hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  const dispatch = queue.dispatch = dispatchAction.bind(
    null,
    currentlyRenderingFiber, // wip Fiber
    queue, // 更新队列
  );
  return [hook.memoizedState, dispatch];
}

mountState中,对mountWorkInProgressHook返回的Hook对象进行初始化。在函数mountWorkInProgressHook内部,创建了新的Hook对象,接着根据全局对象workInProgressHook来管理work in progress Fiber(本文称作新Fiber)的Hook链表将新建的Hook追加到链表末尾最后返回。

js 复制代码
function mountWorkInProgressHook() {
  // 创建新Hook对象
  const hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // 这是当前Fiber上的第一个Hook
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 并非第一个Hook,将新Hook追加到链表末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

再看这里对新建Hook对象的属性处理,其中baseState为上一次更新后得到的基础状态值,这里在挂载阶段由于不存在上一次更新所以直接赋值。接着将状态更新队列存储在queue属性中,例如我们在一次执行阶段触发了对应Hook的dispatch函数,可能会执行多次,如下代码所示:

js 复制代码
const add = () => {
    setCount(count + 1)
    setCount(prev => prev + 1)
    setCount(count + 1)
}

其中basicStateReducer函数参数的逻辑是:如果action是函数,则调用它并传入当前state;否则直接返回action本身。这也正是useStateuseReducer的核心区别------useStatereducer是固定的,而useReducerreducer由开发者自定义。

接着创建状态更新队列中的dispatch函数,其通过dispatchAction函数绑定返回,这里指定thisArg参数为null是因为该函数内部不需要使用到this,最主要还是起到一个预置参数的作用,在返回的dispatch函数被调用时可以直接使用其传递的参数,我们只需要传递剩余的参数即可,这里我们来拆解dispatchAction方法。

js 复制代码
function dispatchAction(fiber, queue, action) {
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(fiber);

  // 创建Update对象
  const update = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  };

  // 追加更新对象到更新队列末尾
  const pending = queue.pending;
  if (pending === null) {
    // 首次更新,直接创建循环链表
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;

  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // 这是渲染阶段产生的更新,需要暂存在一个以更新队列为键的Map中,在渲染阶段结束后再进行暂存更新的处理
    // 这里标记该变量
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else {
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // 当前更新队列为空,意味着我们可以在进入下一个渲染阶段前计算好下一次的状态值
      // 如果状态值没有变化则直接跳过后续处理
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
          const currentState = queue.lastRenderedState;
          const eagerState = lastRenderedReducer(currentState, action);
          // 将预先计算出的状态和用于计算该状态的reducer函数存储在更新对象上
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          // 如果在进入渲染阶段时reducer没有改变,便可直接使用预先该计算的状态,无需再次调用reducer
          if (is(eagerState, currentState)) {
            return;
          }
        } catch (error) {
          ...
        }
      }
    }

    // 开启调度
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

这里我留下了核心的代码部分,其中eventTimelane分别用于时间片调度和优先级调度,最后会传给scheduleUpdateOnFiber进行任务调度,这里我们不做过多关注。总的来说就是创建更新对象并将其追加到更新队列末尾,在渲染阶段产生更新进行标记,但如果预先计算的新状态与当前状态相同(即is(eagerState, currentState)为 true),则会直接返回,跳过后续的调度和重渲染,作为性能优化路径。

更新阶段

在更新阶段我们updateState内部直接返回updateReducer函数的调用结果。

js 复制代码
function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

注意,在更新阶段,updateStateinitialState参数不会被使用。因为此时Hook已经存在,状态直接从旧Fiber树中克隆,初始值只在挂载阶段生效。那我们直接来分析updateReducer函数。

js 复制代码
function updateReducer(reducer, initialArg, init) {
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  const current = currentHook;

  // 上次渲染阶段因优先级不够而跳过的更新队列
  let baseQueue = current.baseQueue;

  // 本次渲染阶段产生的新更新
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    // 存在仍未处理的新更新
    // 将环形链表pendingQueue追加到baseQueue末尾
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    // 修改指针指向现在拼接后的末尾
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  // 接着遍历合并后的基本更新队列
  if (baseQueue !== null) {
    const first = baseQueue.next;
    // 起始状态(旧树的基准状态)
    let newState = current.baseState;

    // 新的基准状态(跳过更新时使用)
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateLane = update.lane;
      // 该更新的优先级不够,需要跳过
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        const clone = {
          lane: updateLane,
          action: update.action,
          eagerReducer: update.eagerReducer,
          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 {

        if (newBaseQueueLast !== null) {
          const clone = {
            lane: NoLane,
            action: update.action,
            eagerReducer: update.eagerReducer,
            eagerState: update.eagerState,
            next: null,
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        if (update.eagerReducer === reducer) {
          // 如果本次更新预先处理过,在dispatchAction函数中已经针对更新队列为空时进行预先计算
          newState = update.eagerState;
        } else {
          // 否则调用reducer计算新状态值
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = newBaseQueueFirst;
    }

    hook.memoizedState = newState; // 更新后的新状态
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState; // 更新在队列中保存的缓存状态
  }

  const dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

updateWorkInProgressHook函数内部,保证了每次调用时新旧Fiber的Hook都能对应上,并且实现首次更新或者渲染中断时复用Hook对象。

js 复制代码
// 通过currentlyRenderingFiber初始化currentHook和wipHook或者通过内部变量管理游标移动,
// 确保第 N 次调用 useState,能对应到第 N 个 current Hook 和 wip Hook
function updateWorkInProgressHook() {
  // 这个函数使用在更新和渲染阶段更新触发的重渲染阶段。该函数假定存在一个可以从旧Fiber树克隆的Hook,或者存在一个来自上次渲染的workInProgress Hook可作为基础值复用。
  let nextCurrentHook;
  // 旧树中不存在该Hook,也就是首次更新
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate; // 旧Fiber
    // 直接复用旧树Hook
    if (current !== null) {
      nextCurrentHook = current.memoizedState; // Hook链表给到nextCurrentHook
    } else {
      // currentHook为空且旧Fiber为空
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  // 最终拿到的nextCurrentHook即为旧Fiber的Hook,通过每次调用函数移动指针指向链表下一个Hook对象

  let nextWorkInProgressHook;
  // 首次更新时,wipHook为空,需要从当前正在渲染的wipFiber中获取Hook链表
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  // 这里保证每次调用函数移动指针指向链表下一个Hook对象

  // 中断恢复时复用
  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    currentHook = nextCurrentHook;
    
    // 根据上一个currentHook克隆新Hook
    const newHook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };

    if (workInProgressHook === null) {
      // 这是列表中第一个Hook
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      添加新Hook到链表末尾
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

总结

useState的底层实现,是作为React函数组件状态管理的基础。其核心通过双缓存Fiber架构 管理Hook对象链表,每个Hook对象的queue对象维护一个环形更新链表,配合优先级机制 ,支持高优先级更新插队、低优先级更新延迟处理,实现可中断的并发渲染。通过eagerState预计算新状态,若与当前状态相同则跳过调度;在updateReducer中复用缓存的eagerState,避免重复计算。

通过理解useState底层原理,我们能够理解React其声明式UI设计思想,而且也更清晰准确地发现和改善日常开发中遇到的一些问题,写出更高效的React代码。

注:本文基于React 18源码进行分析,关键代码都进行了中文注释标记。

相关推荐
光影少年20 小时前
RN 原生动画 Animated 用法、动画性能优化
react native·react.js·掘金·金石计划
YFF菲菲兔1 天前
React 不冒泡的移入 / 移出事件插件
react.js
YFF菲菲兔1 天前
React 表单控件 onChange 事件插件
react.js
濮水大叔2 天前
Hooks 之后,为什么前端仍需要“包裹”能力?
vue.js·react.js·前端框架
小粉粉hhh2 天前
React(三)——Redux
前端·javascript·react.js
YFF菲菲兔2 天前
React 基础事件插件
react.js
qq_529599382 天前
react js循环滚动
前端·javascript·react.js
索西引擎2 天前
【React】严格模式:开发阶段质量保障的静态分析机制
前端·react.js·前端框架
小程故事多_803 天前
拆解SOP+ReAct对话Agent,告别硬编码,解锁智能对话的柔性迭代能力
前端·人工智能·react.js·前端框架