三十七、useState 和 useReducer


面试题:useState 和 useReducer 有什么样的区别?

基本用法

useState 我们已经非常熟悉了,如下:

function App(){
  const [num, setNum] = useState(0);
  
  return <div onClick={()=>setNum(num + 1)}>{num}</div>;
}

接下来我们来看一下 useReducer。如果你会 redux,那么 useReducer 对你来讲是非常熟悉的。

const [state, dispatch] = useReducer(
  reducer,	
  initialArg,	
  init	
);

接下来我们来看一个计数器的例子:

import { useReducer, useRef } from "react";

// 定义一个初始化的状态
const initialState = { count: 0 };

/**
 * reducer
 * @param {*} state 状态
 * @param {*} action 数据变化的描述对象
 */
function counter(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + action.payload };
    case "DECREMENT":
      return { count: state.count - action.payload };
    default:
      return state;
  }
}

function App() {
  // const [num, setNum] = useState(0);
  // 后期要修改值的时候,都是通过 dispatch 来进行修改
  const [state, dispatch] = useReducer(counter, initialState);
  const selRef = useRef();

  const increment = () => {
    // 做自增操作
    // 1. 你要增加多少?
    const num = selRef.current.value * 1;
    // setNum(num);
    dispatch({
      type: "INCREMENT",
      payload: num,
    });
  };

  const decrement = () => {
    const num = selRef.current.value * 1;
    dispatch({ type: "INCREMENT", payload: num });
  };

  const incrementIfOdd = () => {
    const num = selRef.current.value * 1;
    if (state.count % 2 !== 0) {
      dispatch({ type: "INCREMENT", payload: num });
    }
  };

  const incrementAsync = () => {
    const num = selRef.current.value * 1;
    setTimeout(() => {
      dispatch({ type: "INCREMENT", payload: num });
    }, 1000);
  };

  return (
    <div>
    <p>click {state.count} times</p>
    <select ref={selRef}>
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    </select>
    <button onClick={increment}>+</button>
    <button onClick={decrement}>-</button>
    <button onClick={incrementIfOdd}>increment if odd</button>
      <button onClick={incrementAsync}>increment async</button>
    </div>
  );
}

export default App;

useReducer 还接收第三个参数,第三个参数,是一个惰性初始化函数,简单理解就是可以做额外的初始化工作

// 惰性初始化函数
function init(initialState){
  // 有些时候我们需要基于之前的初始化状态做一些操作,返回新的处理后的初始化值
  // 重新返回新的初始化状态
  return {
    count : initialState.count * 10
  }
}

// 接下来在使用 useReducer 的时候,这个函数就可以作为第三个参数传入
const [state, dispatch] = useReducer(counter, initialState, init);

mount 阶段

useState 的 mount 阶段

function mountState(initialState) {
  // 拿到 hook 对象
  const hook = mountWorkInProgressHook();
  // 如果传入的值是函数,则执行函数获取到初始值
  if (typeof initialState === "function") {
    initialState = initialState();
  }
  // 将初始值保存到 hook 对象的 memoizedState 和 baseState 上面
  hook.memoizedState = hook.baseState = initialState;
  const queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  // dispatch 就是用来修改状态的方法
  const dispatch = (queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue
  ));
  return [hook.memoizedState, dispatch];
}

useReducer 的mount阶段

function mountReducer(reducer, initialArg, init) {
  // 创建 hook 对象
  const hook = mountWorkInProgressHook();
  let initialState;
  // 如果有 init 初始化函数,就执行该函数
  // 将执行的结果给 initialState
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = initialArg;
  }
  // 将 initialState 初始值存储 hook 对象的 memoizedState 以及 baseState 上面
  hook.memoizedState = hook.baseState = initialState;
  // 创建 queue 对象
  const queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;
  const dispatch = (queue.dispatch = dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    queue
  ));
  // 向外部返回初始值和 dispatch 修改方法
  return [hook.memoizedState, dispatch];
}

总结一下,mountState 和 mountReducer 的大体流程是一样的,但是有一个区别,mountState 的 queue 里面的 lastRenderedReducer 对应的是 basicStateReducer,而 mountReducer 的 queue 里面的 lastRenderedReducer 对应的是开发者自己传入的 reducer,这里说明了一个问题,useState 的本质就是 useReducer 的一个简化版,只不过在 useState 内部,会有一个内置的 reducer

basicStateReducer 对应的代码如下:

function basicStateReducer(state, action) {
  return typeof action === "function" ? action(state) : action;
}

update 阶段

useState 的 update 阶段

function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

useReducer 的 update 阶段

function updateReducer(reducer, initialArg, init){
  // 获取对应的 hook
  const hook = updateWorkInProgressHook();
  // 拿到对应的更新队列
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  // 省略根据 update 链表计算新的 state 的逻辑
  // 这里有一套完整的关于 update 的计算流程

  const dispatch = queue.dispatch;

  return [hook.memoizedState, dispatch];
}

真题解答

题目:useState 和 useReducer 有什么样的区别?

参考答案:

useState 本质上就是一个简易版的 useReducer。

在 mount 阶段,两者之间的区别在于:

  • useState 的 lastRenderedReducer 为 basicStateReducer
  • useReducer 的 lastRenderedReducer 为传入的 reducer 参数

所以,useState 可以视为 reducer 参数为 basicStateReducer 的 useReducer

在 update 阶段,updateState 内部直接调用的就是 updateReducer,传入的 reducer 仍然是 basicStateReducer。

与AI的对话

在React的源码中,lastRenderedReducer是useReducer Hook的一个内部属性,它用于优化性能。lastRenderedReducer存储的是上一次渲染时使用的reducer函数。这个属性的主要作用是在某些情况下避免不必要的reducer函数调用,从而提高性能。

useReducer的工作原理

useReducer是React提供的一个Hook,用于在函数式组件中管理复杂的状态逻辑。它类似于useState,但是更适合处理多个子状态或复杂的状态转换。

const [state, dispatch] = useReducer(reducer, initialState);
  • reducer是一个函数,它接收当前的状态和动作(action),然后返回新的状态。
  • initialState是初始状态。

lastRenderedReducer的作用

在useReducer的内部实现中,lastRenderedReducer用于比较当前渲染的reducer函数和上一次渲染的reducer函数是否相同。如果它们相同,React可以跳过调用reducer函数,直接使用上一次的状态,从而避免不必要的计算。

优化场景

这种优化在以下场景中特别有用:

  • 组件重新渲染:当组件因为props变化或其他原因重新渲染时,如果reducer函数没有变化,React可以使用上一次的状态,而不是重新调用reducer。
  • 依赖项变化:如果useReducer的依赖项数组(第二个参数)没有变化,React也可以跳过调用reducer。

示例说明

假设我们有以下组件:

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return { count: state.count + 1 };
      case 'decrement':
        return { count: state.count - 1 };
      default:
        return state;
    }
  }

  return (
    <div>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

在这个组件中,useReducer的reducer函数在上一次渲染时已经被调用并存储在lastRenderedReducer中。如果组件重新渲染,并且reducer函数没有变化,React可以直接使用上一次的状态,而不是重新调用reducer。

总结

  • lastRenderedReducer是useReducer Hook的一个内部属性,用于存储上一次渲染时使用的reducer函数。
  • 它的主要作用是优化性能,避免在reducer函数没有变化的情况下重新调用reducer。
  • 这种优化在组件重新渲染或依赖项没有变化时特别有用。

当您点击按钮执行increment操作时,状态值确实会发生变化,即使lastRenderedReducer被复用。这是因为useReducer Hook的设计确保了状态的变化能够被正确地处理和更新。让我们详细解释这个过程。

useReducer的工作流程

当您调用dispatch函数并传入一个action时,useReducer会执行以下步骤:

  1. 调用reducer函数
  • useReducer会使用当前的状态(state)和传入的action调用reducer函数。
  • reducer函数根据action的类型返回新的状态。
  1. 更新状态
  • useReducer会使用reducer函数返回的新状态来更新组件的状态。
  • 这个新状态会替换掉旧的状态,成为组件的当前状态。
  1. 触发组件更新
  • 一旦状态发生变化,React会调度组件的重新渲染,以便组件能够使用新的状态来更新其输出。

lastRenderedReducer的复用

lastRenderedReducer的复用主要是为了优化性能,它并不意味着状态不会更新。实际上,即使lastRenderedReducer被复用,状态更新的流程仍然会正常进行。这是因为:

  • reducer函数的调用:每次dispatch一个action时,reducer函数都会被调用,无论lastRenderedReducer是否被复用。
  • 状态比较:React会比较reducer函数返回的新状态和当前状态是否相同。如果不同,React会更新状态并触发组件的重新渲染。

示例说明

假设我们有以下组件:

function MyComponent() {
 const [state, dispatch] = useReducer(reducer, { count: 0 });
 
 function reducer(state, action) {
   switch (action.type) {
     case 'increment':
       return { count: state.count + 1 };
     case 'decrement':
       return { count: state.count - 1 };
     default:
       return state;
   }
 }
 
 return (
   <div>
     <span>{state.count}</span>
     <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
     <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
   </div>
 );
}
javascript复制代码

在这个组件中,当您点击Increment按钮时,dispatch({ type: ‘increment’ })会被调用,这会导致reducer函数被调用并返回一个新的状态对象{ count: state.count + 1 }。React会使用这个新状态来更新组件的状态,并触发组件的重新渲染。因此,您会看到计数器的值增加了。

总结

  • 即使lastRenderedReducer被复用,状态值仍然会根据reducer函数的返回值更新
  • reducer函数的调用和状态更新是独立的,不受lastRenderedReducer复用的影响
  • React通过比较新旧状态来决定是否需要更新组件,确保状态的变化能够被正确地处理和反映在UI上

是的,您的理解是正确的。reducer的复用是在组件重新渲染时发生的,但这并不意味着状态不会更新。实际上,reducer的复用和状态的更新是两个不同的概念。让我们详细解释这两个过程:

reducer的复用

reducer的复用是指React在组件重新渲染时,会尝试复用上一次渲染时使用的reducer函数。这是为了优化性能,避免在每次渲染时都重新创建reducer函数。lastRenderedReducer属性就是用来存储上一次渲染时使用的reducer函数的。

状态的更新

状态的更新是指当dispatch函数被调用时,reducer函数会根据当前的状态和传入的action来计算新的状态。这个过程与reducer的复用是独立的。即使reducer函数被复用,每次dispatch一个action时,reducer函数仍然会被调用,并根据action来更新状态。

组件的重新渲染

当组件的状态更新后,React会调度组件的重新渲染。在重新渲染过程中,React会使用新的状态来生成新的组件输出(JSX)。这时,React会再次尝试复用reducer函数,但状态已经是新的状态了。

示例说明

假设我们有以下组件:

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { count: 0 });

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

return (
  <div>
    <span>{state.count}</span>
    <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
    <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
  </div>
);
}

在这个组件中,当您点击Increment按钮时,dispatch({ type: ‘increment’ })会被调用,这会导致reducer函数被调用并返回一个新的状态对象{ count: state.count + 1 }。React会使用这个新状态来更新组件的状态,并触发组件的重新渲染。在重新渲染过程中,React会尝试复用reducer函数,但状态已经是新的状态了。

总结

  • reducer的复用是在组件重新渲染时发生的,它不影响状态的更新
  • 每次dispatch一个action时,reducer函数都会被调用,并根据action来更新状态
  • 组件的重新渲染会使用新的状态来生成新的组件输出,这时React会再次尝试复用reducer函数

文章作者: 吴俊杰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 吴俊杰 !
  目录