【React Hooks原理 - useReducer】

发布于:2024-07-11 ⋅ 阅读:(27) ⋅ 点赞:(0)

概述

众所周知useState是基于useReducer来实现状态更新的,在前面我们介绍过useState的原理,所以在这里介绍下useReducer。本文主要从基础使用入手,再进一步从源码来看useReducer实现原理两个方面来介绍useReducer这个Hook。由于本文省略了部分之前提及的代码和流程,为了避免冗余,有兴趣的可以优先浏览这篇文章:【React Hooks原理 - useState】

基础使用

我们都知道useReducer 是通过传入一个 reducer 函数和初始值,返回state和一个更新 dispatcher,通过返回的dispatcher来触发 action 以更新 state,以此来进行状态管理的Hook 。下面我们从代码来看他的使用。

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] 

从定义来看,其接收三个参数和我们上面的描述所对应:

  • reducer: 进行状态更新逻辑的函数
  • 初始值:可以是任意类型的值,也可以是返回值的函数
  • init: 可选,传递初始化函数本身,可以避免组件更新渲染而重新执行初始化函数

这里对第三个参数init函数,进行补充说明:

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, createInitialState(username));
  // ...

上面代码中虽然 createInitialState(username) 的返回值只用于初次渲染,但是在每一次渲染的时候都会被调用。如果它创建了比较大的数组或者执行了昂贵的计算就会浪费性能。所以为了避免重复执行的问题,提供了init参数。

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, username, createInitialState);
  // ...

需要注意的是你传入的参数是 createInitialState 这个 函数自身,而不是执行 createInitialState() 后的返回值。这样传参就可以保证初始化函数不会再次运行。当传入第三个函数时,React会将第二个参username作为createInitialState函数的入参传递,并且只会在初次渲染时执行。

会重复执行的原因是React内部使用Object.is判断值是否改变,而组件每次渲染都会产生一个新的值,所以也可以通过useMemo来缓存createInitialState(username)结果来避免,但React推荐以第三个参数来处理

所以使用reducer进行状态管理的话,需要自己手动写状态更新规则reduer,一下是官网中的一个简单demo:

import { useReducer } from 'react';

function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    };
  }
  throw Error('Unknown action.');
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });

  return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        Increment age
      </button>
      <p>Hello! You are {state.age}.</p>
    </>
  );
}

源码解析

同其他Hooks一样(useContext除外),useReducer也分为mount、update阶段并通过dispatcher根据不同阶段执行不同函数。

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}

下面我们仍然分别介绍useReducer在mount和update阶段分别做了什么。

mount挂载时

相比经过前面几篇文章的学习我们对代码已经相对熟悉了,所以下面会省略部门代码的解释(因为在其他两篇已经解释过,这里不再冗余)。在mountReducer函数中主要做了以下功能(详情可以看代码注释):

  • 创建并挂载当前fiber节点的hook链表
  • 处理初始化函数(如果有)
  • 保存初始值,用于对比和更新
  • 创建当前hook的更新队列(循环链表)
  • 绑定dispatcher用于触发更新reducer
  • 返回包含[value, setValue]
function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: (I) => S
): [S, Dispatch<A>] {
  // 创建初始hook绑定fiber的memoizedState属性
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    // 传递了初始化函数则使用第二个值为入参并执行
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
  // 缓存初始值,和更新的起始值
  hook.memoizedState = hook.baseState = initialState;
  // 创建当前hook的更新队列(循环链表),并将reducer、初始值绑定到更新对象update中
  const queue: UpdateQueue<S, A> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  // 绑定dispatcher为dispatchReducerAction,当通过set函数更新时,调用该函数
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue
  ): any));
  // 返回包含当前state和setState的dispatcher数组
  return [hook.memoizedState, dispatch];
}

首次挂载时,state就只执行了这个函数完成了Function Component -> FIber -> Hook -> update 之间的连接和初始化

这里说的mount、update指的是组件首次渲染和更新渲染时state的操作,并不是执行set函数之后更新state操作

update更新时

在更新渲染时,通过dispatcher派发,最终执行updateReducer函数,其中updateWorkInProgressHook使用复用hook进行性能优化,updateReducerImpl进行更新队列的处理以及状态更新

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: (I) => S
): [S, Dispatch<A>] {
  // 复用hook避免重新创建,优先复用workInprogress.nextHook,没有则克隆页面显示的current.nextHook,都没有则抛出异常
  const hook = updateWorkInProgressHook();
  return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}

updateWorkInProgressHookupdateReducerImpl在介绍useState时都详细介绍过,所以这里简单说明了其功能:

updateReducerImpl函数:

/**
 *
 * hook:指向当前 Fiber 节点正在处理的具体 Hook 实例(即 Hook 链表中的一个节点)。
 * current:指向当前 Fiber 节点中对应的 Hook 实例的当前状态(即已渲染到页面上的状态)。
 */
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等待下次渲染时候更新
            ...
          }
        }

        // 开始更新,如果比较紧急的状态更新则直接处理,否则通过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();
      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];
}
  • 处理更新队列,并发起调度申请
  • 处理跳过的更新,追加到当前更新队列
  • 遍历更新队列,根据优先级判断是否跳过优先级低的任务
  • 符合当前更新的任务,通过进行状态更新,在set函数如果提前计算则直接使用计算后值,没有则通过reducer计算状态
  • 通过Object.is判断值是否变化,进行跳过更新步骤
  • 返回计算后的[hook.memoizedState, dispatch]

至此我们将组件在首次渲染和更新渲染中对于state的处理以及梳理了,下面介绍下当发生交互触发set函数进行状态更新的原理。

触发set更新状态

通过mountReducer介绍我们知道暴露的set函数其实就是通过bind绑定dispatchReducerAction的一个dispatcher,所以我们实际执行的是dispatchReducerAction这个函数

function dispatchReducerAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A
): void {
// 获取本次更新优先级
  const lane = requestUpdateLane(fiber);
 // 创建更新任务
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };
// 判断是否是渲染阶段的更新
  if (isRenderPhaseUpdate(fiber)) {
   // 直接添加到本次渲染队列后面,当渲染完成之后立即执行,即在本次渲染之后就能看到更新结果
    enqueueRenderPhaseUpdate(queue, update);
  } else {
  // 通过事件触发`set`函数进行更新,则将该更新任务添加到更新队列enqueueUpdate中,等待下次渲染更新,并获取当前渲染组件的根fiber节点
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
    // 发起申请调度请求
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

从代码能得知在dispatchReducerAction中就是创建本身更新任务然后添加到更新队列中,并发起调度申请。具体步骤如下:

  • 获取本次更新的优先级
  • 创建本次更新任务
  • 判断本次更新是否是渲染阶段直接发起的,如果是则直接将更新任务添加到当前更新队列中,当本次渲染完成之后会立即执行,即然本次渲染结束后也能看到更新后的状态。如果是通过事件触发,则通过enqueueConcurrentHookUpdate函数将更新任务添加到下次更新队列中等下下次渲染更新,然后发起调度申请,等待更新。

enqueueConcurrentHookUpdate函数如下,就是将本次更新任务update添加到下次更新队列中EnqueueUpdate中,并返回当前更新fiber
的root节点,以便发起调度。

export function enqueueConcurrentHookUpdate<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
  lane: Lane
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  return getRootForUpdatedFiber(fiber);
}

dispatchReducerAction函数中有个这个判断if (isRenderPhaseUpdate(fiber))用于将决定当前更新任务的执行时机,里面提到了当前是否是渲染阶段进行的更新,可能会有些疑惑,所以在这里举个例,简单说明一下:

function MyComponent() {
  const [count, setCount] = useState(0);
  function handleClick() {
    setCount(count + 1);  // 在事件处理器中触发的更新
  }
  if (count > 0) {
    setCount(count + 1);  // 在渲染阶段中触发的更新
  }
  return <button onClick={handleClick}>{count}</button>;
}

如上诉代码在组件中直接执行set函数就是在渲染阶段执行,因为每次渲染执行该函数组件时就会调用set函数更新状态,即这种场景下isRenderPhaseUpdate(fiber) === true,在页面渲染完成之后显示的是更新后的值,而要通过点击按钮触发更新的set任务是在下一次渲染完成之后触发更新的。

useReducer和useState的区别

我们都可以useState是基于useReducer来实现状态管理的,主要区别整理了一下几点:

  • 使用参数:useState接收一个参数,可以是值或者函数。useReducer接收三个参数reducer,state, ?:init
  • useState用于简单数据管理,一般在组件内部顶层定义单个状态,而useReducer用于复杂状态管理,可以将手动更新规则封装在组件外部,可以用于整个项目的状态管理。比如可以通过useReducer + useContext代替Redux进行应用间状态共享
  • useState状态是覆盖式的,所以通过state.key修改无法响应。而useReducer通常是累加式的return {...oldState, ...newState}

利用useReducer实现useState

先看代码demo:

function useState(initialState) {
  return useReducer(basicStateReducer, initialState);
}

// React会自动保存当前的state并作为第一个参数传递给reducer
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

如上面例子所示的流程如下:

第一次渲染:

  • useState(0) 调用 useReducer,basicStateReducer 接受初始状态 0 并返回 [0, dispatch]。
  • dispatch 是内部由 useReducer 生成的函数,用于处理状态更新。

调用 setCount:

  • 调用 setCount(count + 1) 会触发 dispatch,其中 action 是 count + 1,即 1。
  • basicStateReducer 接受当前状态 0 和 action 1,直接返回 1 作为新的状态。

函数式更新:

  • 调用 setCount(prevCount => prevCount + 1) 会触发 dispatch,其中 action 是一个函数 prevCount => prevCount + 1。
  • basicStateReducer 调用这个函数,并传入当前状态 1,函数返回 2 作为新的状态。

总结

总结一下useReduer(useState也大致一样,毕竟useState也是基于reducer来的)在React核心包中主要就在mount、update阶段进行状态初始化和hook、更新队列的创建挂载,然后会一个触发更新的dispatcher,然后当调用该dispatcher时会创建一个更新任务等待Scheduler调度,当之前该更新任务时会在Reconciler协调中进行新的fiber树构造,然后进入update阶段会计算新值,并根据新旧值对比判断是否要更新。流程可以理解为: mount -> set更新 -> 新fiber构造 -> update渲染更新 当然这个流程比较粗糙,这里只是解释下本文提到过的几个点的执行时机和顺序。