【React Hooks原理 - useReducer】

avatar
作者
猴君
阅读量:2

概述

众所周知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渲染更新 当然这个流程比较粗糙,这里只是解释下本文提到过的几个点的执行时机和顺序。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!