UseState与UseReducer性能居然有区别?

开发 前端
UseState与UseReducer性能应该完全一致才对。但实际上,他们的性能并不一样。本文就来聊聊他们的细微差别。

大家好,我卡颂。

稍微深入了解过useState的同学都知道 —— useState其实是预置了reducer的useReducer。具体来讲,他预置的reducer实现如下:

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

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

一个严重的bug

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

bug复现地址[1]

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 props为true​时,点击「reducer + 1按钮」后count不会变化。

图片

disabled为true时,多次点击后count仍显示0

当disabled props为false​时,点击「reducer + 1按钮」后count会加1。

图片

disabled为false时,点击后count加1

现在问题来了,当disabled props为true​时(此时count​为0),我们点击「reducer + 1按钮」5次,然后再点击「Disable按钮」(disabled props​会变为false​),此时count为多少呢?

按照代码逻辑,改变disabled对count不会造成影响,所以他应该保持原始状态不变(即为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的实现中,虽然他是预置了reducer的useReducer,但他预置的reducer的引用是不变的,所以用他实现的文章开篇的例子可以命中优化策略。

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

v18之后的改变

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

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

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

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

总结

在考虑性能优化时,如果useState与useReducer都能满足需要,或许useState是更好的选择。

参考资料

[1]bug复现地址:https://codesandbox.io/s/vigorous-dhawan-mqv463。

[2]v18在线示例:https://codesandbox.io/s/blazing-cdn-pzcpz6?file=/src/App.js:509-519。

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2015-07-30 09:20:26

微软Android Lau

2022-03-18 14:09:52

ReactJavaScript

2019-08-09 15:07:33

TomcatJaegerSpringBoot

2022-03-29 20:10:27

React状态管理

2020-08-17 09:22:30

字符串子串对象

2021-08-03 22:26:46

Go函数分页

2020-12-17 10:23:41

死锁LinuxLockdep

2024-03-18 09:24:12

RocketMQ消息模型分布式

2023-05-25 10:03:40

2022-01-04 09:24:32

Python Excel 表格

2024-06-14 10:26:30

2021-06-25 10:04:47

DevOpsDevSecOps开发

2021-07-12 10:18:35

互联网数据代码

2019-02-12 11:07:49

2018-07-13 17:05:22

SQLMySQL数据库

2022-07-12 10:58:49

Linuxsusudo

2023-10-13 15:48:17

OT系统

2024-01-09 16:14:39

RustGo切片

2020-12-22 13:46:48

APISKD

2011-08-08 14:09:55

dhcpbootp
点赞
收藏

51CTO技术栈公众号