在react中,useState
与useReducer
都是我们在函数组件中定义状态变量的hook
,它们的用法和原理都非常类似,所以我们经常会将它们放在一起比较,从本质上来讲:useState
就是内置了reducer
的useReducer
。
下面我们就从源码角度来解析这两个的hook
实现原理与区别。
关于
useState
具体的执行细节可以查看《React18.2x源码解析:函数组件的加载过程》
1,hook加载处理
首先我们查看这两个hook
加载时的源码实现:
useState
js
const HooksDispatcherOnMount: Dispatcher = {
useState: mountState,
}
查看mountState
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
function mountState(initialState) {
// hook加载工作
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null, // 等待处理的update链表
lanes: NoLanes,
dispatch: null, // dispatchSetState修改方法
lastRenderedReducer: basicStateReducer, # react内置函数,通过action和lastRenderedState计算最新的state
lastRenderedState: initialState, // 上一次的state
};
hook.queue = queue;
const dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
return [hook.memoizedState, dispatch];
}
useReducer
js
const HooksDispatcherOnMount: Dispatcher = {
useReducer: mountReducer,
}
查看mountReducer
方法:
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
function mountReducer(reducer, initialArg, init) {
// hook加载工作
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null, // 等待处理的update链表
lanes: NoLanes,
dispatch: null, // dispatchSetState方法
lastRenderedReducer: reducer, # react内置函数,通过action和lastRenderedState计算最新的state
lastRenderedState: initialState, // 上一次的state
};
hook.queue = queue;
const dispatch = queue.dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber, queue)
return [hook.memoizedState, dispatch];
}
对比mountState
和mountReducer
两个方法源码就可以发现,这两个方法的代码逻辑基本一致。
主要的区别就是: 在定义queue
对象时的lastRenderedReducer
属性的赋值不同:
js
// useState: 内置reducer
lastRenderedReducer: basicStateReducer
// useReducer
lastRenderedReducer: reducer
useState
使用的是react
内置的reducer
方法【basicStateReducer
】。- 而
useReducer
使用的是用户定义的reducer
方法。
这里reducer
方法的作用就是在执行更新操作时:根据当前的state
和参数action
计算最新的newState
。
js
newState = reducer(state, action);
比如react
内置的basicStateReducer
方法:
js
function basicStateReducer(state, action) {
// action就是setCount传入的参数,如果为一个函数,则将state传入进行计算,返回新的state
// 如果不是函数,则action就是最新的state
return typeof action === 'function' ? action(state) : action;
}
2,hook更新处理
我们再来查看这两个hook
更新时的处理:
js
const HooksDispatcherOnUpdate: Dispatcher = {
useState: updateState,
useReducer: updateReducer,
}
js
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
updateState
方法也是调用的updateReducer
,也就是说useState
和useReducer
在更新时都是调用的同一个方法即updateReducer
,所以说这两个hook
在更新时是完全一样的处理逻辑。
通过以上的源码了解,我们就可以得出开头的那个结论:useState
就是内置了reducer
的useReducer
。
3,用法上的区别
根据useState
和useReducer
源码实现上的区别,自然而然就可以得出用法上的区别。
useState
使用的是内置的reducer
方法,它的特点是针对单个状态的处理。useReducer
使用的是用户传入的reducer
方法,它的特点就是可以对多个相关的状态进行整合处理。
比如,常见的使用useReducer
场景:
- 多个表单元素整合处理:
js
import { useState, useReducer } from 'react'
function formReducer(state, action) {
switch (action.type) {
case 'name': {
return {
...state,
name: action.name
}
}
case 'age': {
return {
...state,
age: action.age
}
}
}
}
export default function MyFun(props) {
const [userInfo, setUserInfo] = useReducer(formReducer, { name: '', age: '' })
function handleInputChange(e) {
setUserInfo({ type: e.target.name, [e.target.name]: e.target.value})
}
return (
<div className='MyFun'>
<input type="text" name='name' value={userInfo.name} onChange={handleInputChange} />
<input type="text" name='age' value={userInfo.age} onChange={handleInputChange} />
</div>
)
}
TodoList
的增删改:
js
import { useReducer } from 'react'
function reducer(list, action) {
switch (action.type) {
case 'add': {
return [...list, {id: action.id, value: action.value}]
}
case 'edit': {
const arr = list.map(item => {
if (item.id === action.id) {
return {...item, value: action.value};
}
return item;
})
return arr;
}
case 'delete': {
return list.filter(item => item.id !== action.id)
}
}
}
export default function MyFun(props) {
const [todoList, dispatch] = useReducer(reducer, [])
function handleAdd(row) {
dispatch({ type: 'add', ...row})
}
function handleEdit(row) {
dispatch({ type: 'edit', ...row})
}
function handleDelete(id) {
dispatch({ type: 'delete', id })
}
return (
<div className='MyFun'>
<AddTodo onAdd={handleAdd}></AddTodo>
<TodoList list={todoList} onEdit={handleEdit} onDelete={handleDelete}></TodoList>
</div>
)
}
当然以上的场景useState
也能做到,它们并没有本质的差异,只是因为源码实现不同,在用法上就有所不同,选择使用哪个hook
都是可以的,而useReducer
主要的优点就是可以将状态更新逻辑与事件处理程序分离开来。
4,更新时的区别
其实useState
和useReducer
还有一点不同,即它们在加载时设置的dispatch
方法不同:
js
// useState
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
// useReducer
const dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber, queue)
因为dispatch
方法的不同就导致了它们在更新时有一点差异:
useState
的dispatch
方法即【dispatchSetState
】中有eagerState
优化策略,而useReducer
的dispatch
方法中没有。
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A, // state 1
) {
// 请求更新优先级
const lane = requestUpdateLane(fiber);
// 创建update更新对象
const update: Update<S, A> = {
lane,
action, // state 1
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
# 调度之前的一个优化策略校验: eagerState
// 快速计算出最新的state,与原来的进行对比,如果没有发生变化,则跳过后续的更新逻辑
const alternate = fiber.alternate;
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState: S = (queue.lastRenderedState: any);
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 {
// nothing
}
}
}
// 将更新对象入队
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
// 开启一个新的调度更新任务
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
js
// packages\react-reconciler\src\ReactFiberHooks.new.js
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A, // state 1
) {
// 请求更新优先级
const lane = requestUpdateLane(fiber);
// 创建update更新对象
const update: Update<S, A> = {
lane,
action, // state 1
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
# 没有eagerState策略
// 将更新对象入队
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
// 开启一个新的调度更新任务
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
}
我们可以对比dispatchSetState
和dispatchReducerAction
两个方法,它们的作用完全一致,都是在修改状态之后,开启一个新的调度更新任务,唯一的区别 就是dispatchSetState
方法中多了一个eagerState
优化策略的内容。
js
// eagerState 优化策略
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
} catch (error) {
...
}
eagerState
优化策略的功能是:在执行调度更新之前,快速的计算出最新的state
即eagerState
,然后使用Object.is
方法比较新旧状态是否相等,如果相等则表示满足优化策略,不会开启新的更新任务,以此来进行性能优化。
比如以下案例,当我们使用的是useState
:
js
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
function handleClick() {
setCount(1)
}
return (
<div className='MyFun'>
<div>state: {count}</div>
<button onClick={handleClick}>更新</button>
</div>
)
}
此时点击更新按钮,在dispatchSetState
方法中就会进入eagerState
策略,在计算之后发现状态并没有变化,满足优化策略就不会发起新的调度更新任务。
但如果我们使用的是useReducer
:
js
import { useState, useReducer } from 'react'
function reducer(state, action) {
// state: 当前数据; action: 修改动作
switch (action) {
case 'A': {
return 1
}
case 'B': {
return 2
}
}
}
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState({name: 1})
const [status, setStatus] = useReducer(reducer, 'A')
function handleClick() {
setStatus('A')
}
return (
<div className='MyFun'>
<div>state: {count.name}</div>
<button onClick={handleClick}>更新</button>
</div>
)
}
此时点击更新按钮,因为dispatchReducerAction
方法中没有eagerState
策略,所以会直接发起一个新的调度更新任务。
也就是说:在我们修改状态时,即使数据没有发生真正的变化,组件也重新渲染一次,存在额外的更新浪费。
最后还要补充说一点:正是因为useState
存在eagerState
策略,所以我们在修改引用类型数据 时一定要替换掉原数据,而不是直接修改,比如以下案例:
js
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [obj, setCount] = useState({age: 1})
function handleClick() {
// 错误的用法
obj.age = 2;
setCount(obj)
}
return (
<div className='MyFun'>
<div>state: {obj.age}</div>
<button onClick={handleClick}>更新</button>
</div>
)
}
明明我们已经修改了数据,但因为新旧数据依然是同一个对象,则在使用Object.is
判断时就会返回为true
,满足eagerState
策略,所以不会发起新的调度更新任务,这不符合我们的期望。
5,总结
useState
与useReducer
都是我们在函数组件中定义状态变量的hook
,从本质上来讲useState
是内置了reducer
的useReducer
,但因为它们源码实现上的一些不同就导致它们用法上的不同和更新上的不同。
用法上的不同:
useState
任何场景任何数据类型都可以应用,但是因为内置的reducer
,它侧重于对单个状态的修改处理。useReducer
使用的自定义的reducer
,它侧重于多个相关状态的整合处理。
更新时的不同:
useState
更新时存在eagerState
策略,所以如果我们使用的原始类型数据,eagerState
策略会在一定程度上帮助我们进行更新优化,如果我们使用的引用类型数据,一般我们在修改数据时都期望得到更新,所以要替换掉原数据,而不是直接修改。useReducer
不存在eagerState
策略,所以它更适合处理复杂数据类型,如果只是拿来处理简单的原始数据类型,则有可能出现不必要的更新情况,降低应用的性能。
结束语
以上就是useState
和useReducer
的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!