大家好,我卡颂。
略微深刻理解过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
是更好的抉择。