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

4次阅读

共计 2803 个字符,预计需要花费 8 分钟才能阅读完成。

大家好,我卡颂。

略微深刻理解过 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 是更好的抉择。

正文完
 0