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