从源码角度解析:useState与useReducer的区别

在react中,useStateuseReducer都是我们在函数组件中定义状态变量的hook,它们的用法和原理都非常类似,所以我们经常会将它们放在一起比较,从本质上来讲:useState就是内置了reduceruseReducer

下面我们就从源码角度来解析这两个的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];
}

对比mountStatemountReducer两个方法源码就可以发现,这两个方法的代码逻辑基本一致。

主要的区别就是: 在定义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,也就是说useStateuseReducer在更新时都是调用的同一个方法即updateReducer,所以说这两个hook在更新时是完全一样的处理逻辑。

通过以上的源码了解,我们就可以得出开头的那个结论:useState就是内置了reduceruseReducer

3,用法上的区别

根据useStateuseReducer源码实现上的区别,自然而然就可以得出用法上的区别。

  • 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,更新时的区别

其实useStateuseReducer还有一点不同,即它们在加载时设置的dispatch方法不同:

js 复制代码
// useState
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
// useReducer
const dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber, queue)

因为dispatch方法的不同就导致了它们在更新时有一点差异:

useStatedispatch方法即【dispatchSetState】中有eagerState优化策略,而useReducerdispatch方法中没有。

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);
    }
  }
}

我们可以对比dispatchSetStatedispatchReducerAction两个方法,它们的作用完全一致,都是在修改状态之后,开启一个新的调度更新任务,唯一的区别 就是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优化策略的功能是:在执行调度更新之前,快速的计算出最新的stateeagerState,然后使用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,总结

useStateuseReducer都是我们在函数组件中定义状态变量的hook,从本质上来讲useState是内置了reduceruseReducer,但因为它们源码实现上的一些不同就导致它们用法上的不同和更新上的不同。

用法上的不同:

  • useState任何场景任何数据类型都可以应用,但是因为内置的reducer,它侧重于对单个状态的修改处理。
  • useReducer使用的自定义的reducer,它侧重于多个相关状态的整合处理。

更新时的不同:

  • useState更新时存在eagerState策略,所以如果我们使用的原始类型数据,eagerState策略会在一定程度上帮助我们进行更新优化,如果我们使用的引用类型数据,一般我们在修改数据时都期望得到更新,所以要替换掉原数据,而不是直接修改。
  • useReducer不存在eagerState策略,所以它更适合处理复杂数据类型,如果只是拿来处理简单的原始数据类型,则有可能出现不必要的更新情况,降低应用的性能。

结束语

以上就是useStateuseReducer的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!

相关推荐
一只欢喜28 分钟前
uniapp使用uview2上传图片功能
前端·uni-app
尸僵打怪兽41 分钟前
后台数据管理系统 - 项目架构设计-Vue3+axios+Element-plus(0920)
前端·javascript·vue.js·elementui·axios·博客·后台管理系统
ggome1 小时前
Uniapp低版本的安卓不能用解决办法
前端·javascript·uni-app
Ylucius1 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习
前端初见1 小时前
双token无感刷新
前端·javascript
、昔年1 小时前
前端univer创建、编辑excel
前端·excel·univer
emmm4591 小时前
前端中常见的三种存储方式Cookie、localStorage 和 sessionStorage。
前端
Q186000000001 小时前
在HTML中添加视频
前端·html·音视频
bin91531 小时前
前端JavaScript导出excel,并用excel分析数据,使用SheetJS导出excel
前端·javascript·excel
Rattenking2 小时前
node - npm常用命令和package.json说明
前端·npm·json