小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
背景
useState的基本用法
const [state, setState] = useState(init)
例子
function Counter() {
const [count, setCount] = React.useState(() => 0)
const increment = () => setCount(previousCount => previousCount + 1)
return <button onClick={increment}>{count}</button>
}
例子中的useState和更新函数setCount不同于直接使用固定值为初始化和更新后的值,而都是接收了一个函数,且以函数返回值分别作为count的初始值和更新后的值。两种方式在某些特定场景下,会有一定区别。
惰性初始化函数
例子:
const initialState = Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(initialState)
当函数组件更新re-render时,函数组件内所有代码都会重新执行一遍。此时initialState的初始值是一个相对开销较大的IO操作。每次函数组件re-render时,第一行代码都会被执行一次,引起不必要的性能损耗。
const initialState = () => Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(initialState)
当initialState以函数形式传入时,它只会在函数组件初始化的时候执行一次,函数re-render时不会再被执行。这个函数即惰性初始化函数这个特性,可以在这种场景下规避不必要的性能问题。
更新函数
例子:
function DelayedCounter() {
const [count, setCount] = React.useState(0)
const increment = async () => {
await doSomethingAsync()
setCount(count + 1)
}
return <button onClick={increment}>{count}</button>
}
假设doSomethingAsync这个异步函数执行需要500ms,连续快速三次点击按钮,会发现最终count值为1,而不是我们想要的最新值3。而在increment函数内部打印console.log可以发现,函数increment确实执行了三次,但是如果在setCount方法上方console.log(count)打印会发现count值一直是0。
目前我理解的原因是函数组件在一次re-render完成之前,我们连续三次点击按钮,调用increment方法时,setCount所访问的count值一直是未更新的值0导致的。
function DelayedCounter() {
const [count, setCount] = React.useState(0)
const increment = async () => {
await doSomethingAsync()
setCount(previousCount => previousCount + 1)
}
return <button onClick={increment}>{count}</button>
}
这种场景解决方法也很简单,把setCount参数改为函数形式即可。原因是不同于直接取值count,函数默认参数previousCount为count更新后的最新值,所以可以确保每次点击按钮,increment访问的都是最新的count值
总结
- 惰性初始化函数在某些场景下可以规避性能问题,提升性能
- 更新函数默认参数可以确保每次访问的值是更新后的值
参考
React-hooks
useState lazy initialization and function updates