关于前端:useState与useReducer性能居然有区别

大家好,我卡颂。

略微深刻理解过useState的同学都晓得 —— useState其实是预置了reduceruseReducer。具体来讲,他预置的reducer实现如下:

function basicStateReducer(state, action) {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}

那按理来说,useStateuseReducer性能应该完全一致才对。但实际上,他们的性能并不一样。本文就来聊聊他们的细微差别。

欢送退出人类高质量前端交换群,带飞

一个重大的bug

v18之前,特定场景下,useReducer存在一个重大的bug。假如咱们要挂载如下App组件:

bug复现地址

function App() {
  const [disabled, setDisabled] = React.useState(false);
  return (
    <>
      <button onClick={() => setDisabled((prev) => !prev)}>Disable</button>
      <div>{`Disabled? ${disabled}`}</div>
      <CounterReducer disabled={disabled} />
    </>
  );
}

通过点击按钮,能够切换disabled状态,并将disabled作为props传递给CounterReducer组件。

CounterReducer组件的实现如下:

function CounterReducer({ disabled }) {
  const [count, dispatch] = useReducer((state) => {
    if (disabled) {
      return state;
    }
    return state + 1;
  }, 0);
  return (
    <>
      <button onClick={dispatch}>reducer + 1</button>
      <div>{`Count ${count}`}</div>
    </>
  );
}

count状态初始为0,当disabled propstrue时,点击reducer + 1按钮count不会变动。

disabled propsfalse时,点击reducer + 1按钮count会加1。

当初问题来了,当disabled propstrue时(此时count为0),咱们点击reducer + 1按钮5次,而后再点击Disable按钮disabled props会变为false),此时count为多少呢?

依照代码逻辑,扭转disabledcount不会造成影响,所以他应该放弃原始状态不变(即为0)。

但在v18之前,他会变成5。

然而,如果咱们用useState实现同样逻辑的useReducer

function CounterState({ disabled }) {
  const [count, dispatch] = useState(0);

  function dispatchAction() {
    dispatch((state) => {
      if (disabled) {
        return state;
      }
      return state + 1;
    });
  }

  return (
    <>
      <button onClick={dispatchAction}>state + 1</button>
      <div>{`Count ${count}`}</div>
    </>
  );
}

就能获得合乎预期的成果。

所以说,useReducer的实现在非凡场景下是有bug的(v18之前)。

bug是如何产生的

产生这个bug的起因在于React外部的一种被称为eager state的性能优化策略。

简略的说,对于相似如下这样的,即便屡次触发更新,但状态的最终后果不变的状况(在如下例子中count始终为0):

function App() {
  const [count, dispatch] = useState(0);
  return <button onClick={() => dispatch(0)}>点击</button>;
}

App组件是没有必要render的。这就省去了render的性能开销。

要命中eager state,有个严格的前提 —— 状态更新前后不变。

咱们晓得,React中有两种更新状态的形式:

  1. 传递新的状态
// 定义状态
const [count, dispatch] = useState(0);

// 更新状态
dispatch(100)
  1. 传递更新状态的函数
// 定义状态
const [count, dispatch] = useState(0);

// 更新状态
dispatch(oldState => oldState + 100)

那么,对于形式1,要保障状态不变很简略,只须要全等比拟变动前后的状态,如果他们统一就能进入eager state策略。

对于形式2,就稍微简单点,须要同时满足2个条件:

  1. 状态更新函数自身不变
  2. 通过状态更新函数计算出的新状态也不变

比方,下述代码就同时满足2个条件,但如果将change放到App内就不满足条件1(App组件每次render时都会创立新的change函数):

// 状态更新函数自身不变
function change(oldState) {
  // 新状态也不变
  return oldState;
}

function App() {
  const [count, dispatch] = useState(0);
  
  // 状态更新函数每次render都会变动
  // function change(oldState) {
     // 新状态不变
     // return oldState;
  // }
  
  return <button onClick={() => dispatch(change)}>点击</button>;
}

相似的状况,在useState的实现中,尽管他是预置了reduceruseReducer,但他预置的reducer的援用是不变的,所以用他实现的文章开篇的例子能够命中优化策略。

useReducer在特定场景下的bug就与此相关。并不是说bug产生的起因是useReducer肯定没命中优化策略,而是说相比于useState,他命中优化策略很不稳固。

v18之后的扭转

既然bug来源于不稳固的性能优化策略,在没有完满的解决方案之前,React是如何在v18中修复这个bug的呢?

答案是 —— 移除useReducereager state策略。也就是说,在任何状况下,useReducer都不再有useState存在的这个性能优化策略了。

这就导致在特定场景下,useReducer的性能弱于useState

比方在这个v18在线示例中,同样的逻辑用useState实现,不会有冗余的render,而useReducer会有。

总结

在思考性能优化时,如果useStateuseReducer都能满足需要,或者useState是更好的抉择。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理