大家好,我卡颂。
略微深刻理解过 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 复现地址
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 props
为false
时,点击 reducer + 1 按钮 后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
中有两种更新状态的形式:
- 传递新的状态
// 定义状态
const [count, dispatch] = useState(0);
// 更新状态
dispatch(100)
- 传递更新状态的函数
// 定义状态
const [count, dispatch] = useState(0);
// 更新状态
dispatch(oldState => oldState + 100)
那么,对于形式 1,要保障状态不变很简略,只须要全等比拟变动前后的状态,如果他们统一就能进入 eager state
策略。
对于形式 2,就稍微简单点,须要同时满足 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 在线示例中,同样的逻辑用 useState
实现,不会有冗余的 render
,而useReducer
会有。
总结
在思考性能优化时,如果 useState
与useReducer
都能满足需要,或者 useState
是更好的抉择。