【React Hooks原理 - useState】

概述

useState赋予了Function Component状态管理的能力,可以让你在不编写 class 的情况下使用 state 。其本质上就是一类特殊的函数,它们约定以 use 开头。本文从源码出发,一步一步看看useState是如何实现以及工作的。

基础使用

javascript 复制代码
function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] 

在组件顶层通过useState申明状态,useState接收一个参数作为该状态的初始值,该参数可以是任意类型的值,也可以是返回初始值的函数,React内部会进行判断然后执行并缓存该结果。返回一个包含当前状态和更新状态回调的数组

javascript 复制代码
const [state, setState] = useState(initialState)

所有的Hooks都必须在组件顶层或者自定义Hook顶层中使用,不能在条件、循环语句中使用,这是为了避免Hook的位置混乱导致React无法正确找到状态和Hooks的对应而发生错误

源码解析

前面的文章我们提到过,我们使用的Hooks在React18之后在React内部拆分mount、update两个函数,并由dispatcher管理。下面我们将从入口、mount、update这三个方面来介绍useState的源码实现。

入口:文件路径在react/packages/react/src /ReactHooks.js

javascript 复制代码
export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

我们在组件内调用useState申明状态,实际就是执行的这个函数,然后通过dispatcher根据渲染阶段分发执行那个函数。

Mount挂载时

在挂载时,主要调用了mountStatemountStateImplmountWorkInProgressHook下面一一讲解。

mountState: 挂载时调用的函数,接收组件内传入的初始值initialState,然后返回当前状态和更新状态的回调

  • 调用mountStateImpl在当前渲染fiber节点中创建一个hook list用于对所有hook进行管理
  • 根据当前渲染fiber和更新队列创建一个dispatch用于更新状态,由于将dispatch绑定到dispatchSetState上,所以当我们通过set函数更新时,实际执行的是dispatchSetState函数
  • 将dispatch挂载到queue更新队列中,便于后续更新时候直接调用dispatch更新
  • 返回包含当前状态和更新回调的数组
javascript 复制代码
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  // 将dispatch绑定到dispatchSetState,所以当我们通过set函数更新时,实际执行的是dispatchSetState函数
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

hook和queue的数据结构如下:

javascript 复制代码
hook = {
   memoizedState: initialValue, // 保存当前的状态值
   baseState: initialValue,     // 初始状态,用于批处理
   baseQueue: null,				// 需要更新的队列
   queue: null,                 // 更新队列,用于存储状态更新
   next: null                   // 链表中的下一个 Hook 节点
};

const queue = {
    pending: null, // 指向尚未处理的更新。这些更新将在下一次渲染时被处理。
    lanes: NoLanes, // 优先级
    dispatch: null, // 用于分发动作(actions)触发状态更新。
    lastRenderedReducer: basicStateReducer, // 最后一次渲染时使用的 reducer 函数。对于 useReducer,它是用户定义的 reducer 函数;对于 useState,它是一个内置的基本状态 reducer
    lastRenderedState: (initialState: any), // 保存了上一次渲染时 Hook 的状态,以便在下一次渲染时能够对比新旧状态并进行必要的更新。
  };

在mountStateImpl函数中:

  • 调用mountWorkInProgressHook生成hook
  • 缓存初始值(如果是函数则执行并缓存结果),其中memoizedState会在update时计算新值,而baseState则记录初始值在批处理的时候会使用
  • 基于生成的hook初始化更新队列queue
  • 返回这个hook链表给到mountState继续处理
javascript 复制代码
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialState = initialStateInitializer();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}

mountWorkInProgressHook主要就是创建初始化一个hook链表,并将其挂载到当前渲染的fiber节点中。

javascript 复制代码
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

至此useState在挂载时的整个流程就完成了,我们在这里小结一下。当我们在组件中使用useState时,其背后流程就是: Function Component -> FIber节点 -> Hooks链表 -> UpdateQueue -> dispatch(updateState)。每个函数组件都有一个对应的 Fiber 节点,每个 Fiber 节点都有一个 Hook 链表(比如保存组件中的useState、useEffect等所有hook),用于存储该组件中的所有 Hook。而Hook链表中的每个Hook都有一个UpdateQueue更新队列来对状态进行更新,在更新时会依次遍历这个Hooks链表然后执行对应Hook的更新。

fiber、hook、queue关系图:

Update更新时

上面我们介绍了在使用useState初始挂载一个状态时做了什么工作,现在来看看当组件重新渲染时useState是怎么实现的。

javascript 复制代码
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, initialState);
}

从源码来看,如果使用过redux的同学可能会很熟悉,就和我们理解的一致也是通过dispatch来调用Reducer来进行updateState的。由此能看出useState 是基于 useReducer 实现,通过调用updateReducer来实现state更新,其中basicStateReducer 是React内部默认的处理状态更新的reducer。

javascript 复制代码
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: (I) => S
): [S, Dispatch<A>] {
  const hook = updateWorkInProgressHook();
  return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}

通过调用updateReducer将用于状态更新的reducer和初始值以及通过updateWorkInProgressHook处理后的链表传递给了updateReducerImpl进行处理。由于updateReducerImpl中代码比较多而且还涉及到了Scheduler调度中的优先级,所以对于部分跳过的更新逻辑在这里进行了省略,有兴趣的可以去官网查看【React Github

updateWorkInProgressHook代码如下:该函数主要逻辑就是复用已有的hook并更新workInProgressHook指针指向下一个hook。优先复用当前渲染中的hook即workInProgress树中当前fiber的hook,如果没有则克隆页面显示的current fiber中的hook,如果都没有则通过throw抛出异常

updateWorkInProgressHook函数处理并更新指针到下一个hook是因为当前hook在上一次渲染或挂载过程中已经执行并存储了状态。这是因为React需要保持对hook链表的引用,以便在下一次渲染时可以复用这些hook。删除hook可能会导致状态丢失和链表结构破坏。所以在React的实现中,不会在上一次渲染时删除hook,而是在下一次渲染时更新指针。

javascript 复制代码
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook; // 下次更新的hook
// nextCurrentHook值为当前渲染中fiber的下一次hook或者复用页面显示的当前fiber的下一个hook
if (currentHook === null) {
   // 通过alternate指针切换workInProgress和current fiber树
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
    nextCurrentHook = current.memoizedState;
    } else {
    nextCurrentHook = null;
    }
} else {
    nextCurrentHook = currentHook.next;
}

let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
    nextWorkInProgressHook = workInProgressHook.next;
}

if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
} else {
    // Clone from the current hook.

    if (nextCurrentHook === null) {
    const currentFiber = currentlyRenderingFiber.alternate;
    if (currentFiber === null) {
        // This is the initial render. This branch is reached when the component
        // suspends, resumes, then renders an additional hook.
        // Should never be reached because we should switch to the mount dispatcher first.
        throw new Error(
        'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
        );
    } else {
        // This is an update. We should always have a current hook.
        throw new Error('Rendered more hooks than during the previous render.');
    }
    }
    currentHook = nextCurrentHook;
    const newHook: Hook = {
    memoizedState: currentHook.memoizedState,
    baseState: currentHook.baseState,
    baseQueue: currentHook.baseQueue,
    queue: currentHook.queue,
    next: null,
    };
    if (workInProgressHook === null) {
    // This is the first hook in the list.
    currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
    // Append to the end of the list.
    workInProgressHook = workInProgressHook.next = newHook;
    }
}
return workInProgressHook;
}

updateReducerImpl代码如下:

javascript 复制代码
/**
 *
 * hook:指向当前 Fiber 节点正在处理的具体 Hook 实例(即 Hook 链表中的一个节点)。
 * current:指向当前 Fiber 节点中对应的 Hook 实例的当前状态(即已渲染到页面上的状态)。
 * reducer触发active进行state的更新
 */
function updateReducerImpl<S, A>(
  hook: Hook,
  current: Hook,
  reducer: (S, A) => S
): [S, Dispatch<A>] {
  // 获取当前指向hook的更新队列,以及绑定reducer更新函数
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  let baseQueue = hook.baseQueue;

  // 如果有上次渲染未处理的更新队列
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    // 有上次为处理的更新以及本次也有需要处理的更新,则将两个更新队列合并,否则将上次未处理的赋值给更新队列等待本次渲染更新
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  // 如果本次没有更新队列,则更新memoizedState为baseState
  const baseState = hook.baseState;
  if (baseQueue === null) {
    hook.memoizedState = baseState;
  } else {
    // 更新队列有状态需要更新
    const first = baseQueue.next;
    let newState = baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast: Update<S, A> | null = null;
    let update = first;
    let didReadFromEntangledAsyncAction = false;
    do {
      const updateLane = removeLanes(update.lane, OffscreenLane);
      const isHiddenUpdate = updateLane !== update.lane;
      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);
      // 根据优先级判断当前是否需要跳过更新,并保存在newBaseQueueLast中在下次渲染时更新,然后调用markSkippedUpdateLanes跳过本次更新
      if (shouldSkipUpdate) {
        ...
      } else {
        const revertLane = update.revertLane;
        // 根据优先级判断当前是否需要跳过更新,并保存在newBaseQueueLast中在下次渲染时更新
        if (!enableAsyncActions || revertLane === NoLane) {
          ...
        } else {
          // 将符合本次更新条件的状态保存在update链表中,等待更新
          if (isSubsetOfLanes(renderLanes, revertLane)) {
            update = update.next;
            if (revertLane === peekEntangledActionLane()) {
              didReadFromEntangledAsyncAction = true;
            }
            continue;
          } else {
            // 不符合的保存在newBaseQueueLast等待下次渲染时候更新
            ...
          }
        }

        // 开始更新,当前update对象是否提前计算,否则通过reducer处理
        const action = update.action;
        if (update.hasEagerState) {
          // If this update is a state update (not a reducer) and was processed eagerly,
          // we can use the eagerly computed state
          newState = ((update.eagerState: any): S);
        } else {
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    // 遍历本次更新队列之后,判断是否有跳过的更新,如果有则保存在newBaseState中,等待下次渲染时更新
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // 判断上一次的状态和reducer更新之后的状态是否一致,发生变化则通过markWorkInProgressReceivedUpdate函数给当前fiber打上update标签
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
      // 检查异步处理操作,如果说是异步获取值,则需要通过throw entangledActionThenable将当前更新挂起
      if (didReadFromEntangledAsyncAction) {
        const entangledActionThenable = peekEntangledActionThenable();
        if (entangledActionThenable !== null) {
          throw entangledActionThenable;
        }
      }
    }

    // 将本次新的state保存在memoizedState中
    hook.memoizedState = newState;
    // 保存下次更新的初始值,如果本次没有跳过更新,该值为更新后通过reducer或者eagerState计算的新值,有跳过的更新则会本次更新前原来的初始值
    hook.baseState = newBaseState;
    // 将本次跳过的更新保存在baseQueue更新队列中中,下次渲染时更新
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

  // 没有状态更新时,将当前队列优先级设置为默认
  if (baseQueue === null) {
    queue.lanes = NoLanes;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

根据上面代码以及注释可知,在updateReducerImpl主要做了一下功能:

  • 获取和合并更新队列。
  • 遍历更新队列,根据优先级判断是否需要跳过,计算新的更新队列
  • 通过reducer进行状态更新
  • 更新基础状态和队列
  • 处理异步操作的影响
  • 返回新的状态和 dispatch 函数

useState于update的差异

根据下面表格可以简要总结两者差异主要是:mountState进行初始化挂载,updateState在其基础上进行更新队列处理,包括跳过低优先级任务以及提前处理任务。

比较点 mountState updateState
创建 Hook 和更新队列
返回值 返回包含当前状态值和 set 函数的数组 返回包含当前状态值和 set 函数的数组
用途 主要用于组件的首次渲染,初始化数据挂载 处理组件的更新渲染,包含对上次渲染跳过的部分进行处理
初始状态处理 计算并设置初始状态(如果 initialState 是函数,会调用它) 不处理初始状态,只处理更新
更新队列 创建新的更新队列,并将其分配给 Hook 处理现有更新队列,可能需要合并新的和已有的更新队列
状态计算 直接设置初始状态 通过调用 reducer(通常是 basicStateReducer)计算新状态
Hook 链表 将 Hook 添加到当前 Fiber 的 Hook 链表中 更新当前 Fiber 的 Hook 链表中的现有 Hook
优先级处理 不涉及优先级处理 可能需要处理优先级和跳过的更新

set函数

由上面知道了,使用useSatte会返回包含[newState,setValue]的数组,然后我们调用setValue可以更新状态值,而这个dispatch实际是执行的dispatchSetState(在mountState中通过bind进行了绑定),所以下面来看看在dispatchSetState函数中做了什么?

javascript 复制代码
// 触发更新后,React会判断当前是否还有其他渲染或者挂起,没有会提前计算值,不变则跳过更新
function dispatchSetState<S, A>(
    fiber: Fiber,
    queue: UpdateQueue<S, A>,
    action: A,
  ): void {
    // 获取优先级
    const lane = requestUpdateLane(fiber);
    
    // 创建一个update更新对象
    const update: Update<S, A> = {
      lane,
      revertLane: NoLane,
      action,
      hasEagerState: false,
      eagerState: null,
      next: (null: any),
    };
    // 当前是否是渲染阶段
    if (isRenderPhaseUpdate(fiber)) {
        // 申请加入正在渲染的更新队列
      enqueueRenderPhaseUpdate(queue, update);
    } else {
        // 页面上显示的fiber树 - 双缓冲树,通过alternate切换指针
      const alternate = fiber.alternate;
      // 判断current树和workInProgress树是否有挂起的更新,如果没有则进入提前计算逻辑
      if (
        fiber.lanes === NoLanes &&
        (alternate === null || alternate.lanes === NoLanes)
      ) {
        const lastRenderedReducer = queue.lastRenderedReducer;
        if (lastRenderedReducer !== null) {
          let prevDispatcher = null;
          try {
            const currentState: S = (queue.lastRenderedState: any);
            // 用于update state的reducer,由React内部提供,所以这里使用缓存的lastRenderedReducer可以提高性能
            const eagerState = lastRenderedReducer(currentState, action);
            // 标识该值已经提前计算
            update.hasEagerState = true;
            update.eagerState = eagerState;
            if (is(eagerState, currentState)) {
                // 无更新则跳过更新步骤
              enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
              return;
            }
          } catch (error) {
            // Suppress the error. It will throw again in the render phase.
          } finally {
          }
        }
      }
      
      // 进入组件更新步骤 enqueueConcurrentHookUpdate会将update更新对象添加到enqueueUpdate更新队列中
      const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
      if (root !== null) {
        // 进入Scheduler调度阶段,进行fiber构造
        scheduleUpdateOnFiber(root, fiber, lane);
        entangleTransitionUpdate(root, queue, lane);
      }
    }
}

从代码中能知道在函数中主要:

  • 创建一个带有优先级的update对象
  • 判断当前是否是渲染更新阶段,如果是则直接将update对象添加到更新队列中等待更新,否则判断current树和workInProgress树是否有挂起的更新,如果没有则进入提前计算并判断值是否变化,没有变化则跳过更新,否则就进入组件更新步骤将update对象添加到enqueueUpdats更新队列中然后调用scheduleUpdateOnFiber等待调度更新

其中enqueueConcurrentHookUpdate函数主要就是将创建的update对象添加到当前fiber的更新队列enqueueUpdate中,其中通过MapSet来对渲染更新数据进行管理

javascript 复制代码
function enqueueRenderPhaseUpdate<S, A>(
    queue: UpdateQueue<S, A>,
    update: Update<S, A>,
  ) {
   if (renderPhaseUpdates === null) {
     renderPhaseUpdates = new Map();
   }
   let firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
   if (firstRenderPhaseUpdate === undefined) {
     renderPhaseUpdates.set(queue, update);
   } else {
     // Append the update to the end of the list.
     let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
     while (lastRenderPhaseUpdate.next !== null) {
       lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
     }
     lastRenderPhaseUpdate.next = update;
   }
 }

将本次更新的update对象添加到更新队列中,会调用scheduleUpdateOnFiber等待调度更新然后进行新的fiber树构造。

这里从宏观角度,简单说下React多个包之间的流程来帮助我们理解。 React中有几个核心包:react、react-dom、react-reconciler(协调)、scheduler(调度),其中上面的update更新对象处理以及将其添加到更新队列中是在react包中处理的,然后调用scheduler包中的scheduleUpdateOnFiber等待调度进入react-reconciler包中处理进行renderer阶段进行fiber构造,最后进入react-dom进行commit阶段进行页面的渲染。

上图就是React总的流程图,各个核心流程都在里面,有兴趣的同学可以查看我写的其他React源码系列,比如【React架构 - Fiber构造循环

总结

上面说了这么多这里进行简单总结一下,有的点可能会多次提及为了巩固记忆。

正文开始~~

在页面渲染过程中有两个阶段分别为mount(首次渲染)、update(更新渲染),而React为了更好的管理和优化副作用将Hooks(useContext除外)拆为了mount、update两个函数。通过内置的dispatcher管理,React会根据目前具体处于什么阶段来决定调用那个函数,比如在mount阶段,会调用mountState函数,这对于开发者来说是无感的,React在内部进行了映射。具体的流程走向如下图所示

mount、update时,state的处理:

当触发set函数进行状态更新时:

以上都是根据自己理解进行总结梳理的,如果理解有误还请评论指正。

相关推荐
轻口味42 分钟前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王1 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发1 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6414 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js