如果你已经用React工作了一段时间,你可能已经使用了useState 。 这里有一个快速的API例子:
function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(count + 1)
return <button onClick={increment}>{count}</button>
}
所以,你用初始状态值调用useState ,然后它返回一个数组,其中包含该状态的值和更新该状态的机制(这被称为 "状态dispatch 函数")。当你调用状态调度函数时,你传递新的状态值,这将触发组件的重新渲染,导致再次调用useState来获取新的状态值和调度函数。
当你进入React时,这是关于状态的第一件事(至少,如果你从我的免费课程中学习的话是这样)。但是useState 调用和dispatch 函数都有一个鲜为人知的特点,有时会很有用:
function Counter() {
const [count, setCount] = React.useState(() => 0)
const increment = () => setCount(previousCount => previousCount + 1)
return <button onClick={increment}>{count}</button>
}
不同的是,本例中的useState 是用一个返回初始状态的函数来调用的(而不是简单地传递初始状态),而setCount (dispatch)是用一个接受先前状态值并返回新状态的函数来调用的。这与前面的例子功能完全相同,但有一些微妙的区别,我们将在这篇文章中讨论。我在我的免费React课程中也讲过这个问题,但我被问到的次数太多了,所以我想我也要写一下。
useState 懒惰的初始化
如果你在Counter函数的函数体中添加一个console.log ,你会发现每次点击按钮时都会运行该函数。这是有道理的,因为你的Counter函数是在每个渲染阶段运行的,点击按钮会触发状态更新,从而引发重新渲染。你需要记住的一点是,如果函数主体运行,就意味着它里面的所有代码也会运行。这意味着你创建的任何变量或传递的参数都会在每次渲染时被创建和评估。这通常不是什么大问题,因为JavaScript引擎非常快,可以对这种事情进行优化。所以像这样的事情是没有问题的。
const initialState = 0
const [count, setCount] = React.useState(initialState)
然而,如果你的状态的初始值在计算上是昂贵的呢?
const initialState = calculateSomethingExpensive(props)
const [count, setCount] = React.useState(initialState)
或者,更实际的是,如果你需要读入localStorage ,这是一个IO操作,怎么办?
const initialState = Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(initialState)
请记住,React唯一需要初始状态的时候是最初😉意思是,它只在第一次渲染时真正需要初始状态。但是因为我们的函数体在每次重新渲染组件时都会运行,所以我们最终会在每次渲染时都运行该代码,即使它的值没有被使用或需要。
这就是懒惰初始化的意义所在。它允许你把这些代码放在一个函数中。
const getInitialState = () => Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(getInitialState)
创建一个函数是很快的。即使这个函数所做的事情在计算上很昂贵。所以你只有在调用函数时才会付出性能上的代价。所以如果你把一个函数传给useState ,React只会在需要初始值时(也就是组件最初被渲染时)调用该函数。
这被称为 "懒惰初始化"。这是一个性能优化。你不应该经常使用它,但它在某些情况下是有用的,所以知道它是一个存在的功能,并且你可以在需要时使用它,是一件好事。我想说我只有2%的时间使用这个功能。这不是一个我经常使用的功能。
dispatch 功能更新
这个就有点复杂了。解释它的作用的最简单方法是通过展示一个例子。比方说,在我们更新计数状态之前,我们需要做一些异步操作。
function DelayedCounter() {
const [count, setCount] = React.useState(0)
const increment = async () => {
await doSomethingAsync()
setCount(count + 1)
}
return <button onClick={increment}>{count}</button>
}
比方说,这个异步的事情需要500ms。这就是在这里呈现的,点击这个按钮三次,非常快。
console.log 如果你点击的速度够快,你会发现计数只增加到了1。但这很奇怪,因为如果你在increment,你会发现它被调用了三次(每次点击一次),那么为什么计数状态没有更新三次?
嗯,这就是事情变得棘手的地方。事实是,状态被更新了三次(每次点击一次),但是如果你在调用setCount 的正上方添加一个console.log(count),你会发现count ,每次都是0 !所以我们每次点击都会调用setCount(0 + 1) ,尽管我们实际上想增加计数。
那么,为什么每次的计数都是0 ?这是因为我们通过onClick 道具给React的increment 函数在创建button 时 "关闭 "了count 的值。你可以从cdn.io/closure中了解更多关于闭包的信息,但简而言之,当一个函数被创建时,它可以访问定义在它之外的变量,即使这些变量被分配的内容发生了变化。
count 问题是,我们实际上是在调用完全相同的increment 函数,该函数在有机会根据之前的点击进行更新之前,就已经关闭了0 的值,并且一直保持这种状态,直到React重新渲染。
所以你可能会认为,如果我们能在异步操作之前触发重新渲染,就能解决这个问题,但这对我们来说也是不行的。这种方法的问题在于,虽然increment 在这次渲染中可以访问count ,但在下一次渲染中却不能访问count 这个变量。事实上,在下一次渲染中,我们会有一个完全不同的increment函数,它可以访问一个完全不同的count 变量。因此,我们每次渲染都会有两份所有变量的副本。(垃圾收集通常会清理这些副本,我们只需要担心现在的问题)。但是由于count 还没有被更新,当increment 的新副本被创建时,count 的值仍然是0 ,这就是为什么当我们第二次点击按钮时,0 和第一次一样。
你会注意到,如果你在再次点击按钮之前等待计数值的更新,一切都会正常,这是因为我们已经等待了足够长的时间来重新渲染,一个新的increment 函数已经被创建,并带有最新的count 的值。
但显然,这有点令人困惑,而且也有点问题。那么,解决方案是什么呢?函数更新!我们真正需要的是在我们进行更新时,用某种方法来确定count 的前一个值,这样我们就可以根据count的前一个值来确定它。
任何时候我需要根据以前的状态来计算新的状态,我都会使用函数更新。
所以这里是解决方案。
function DelayedCounter() {
const [count, setCount] = React.useState(0)
const increment = async () => {
await doSomethingAsync()
setCount(previousCount => previousCount + 1)
}
return <button onClick={increment}>{count}</button>
}
在这里试试。
你可以随心所欲地点击它,它将管理更新每次点击的计数。这样做的原因是我们不再担心访问一个可能是 "陈旧 "的值,而是获得我们需要的变量的最新值。因此,即使我们正在运行的increment 函数有一个较旧的版本count ,我们的函数更新器也会收到最新的状态版本。
我应该补充一点,如果你使用useReducer,就不会有这样的问题,因为那会一直收到最新版本的state (作为你的reducer的第一个参数),所以你不需要担心状态值过期。不过,如果你的状态更新是由道具或一些外部状态决定的,这时你可能需要一个useRef 来帮助确保你得到该道具或外部状态的最新版本。但这是另一篇博文的主题......
结语
我希望这有助于解释useState lazy initializers和dispatch function updates的内容/原因/方法。懒惰的初始化器对于改善某些情况下的性能问题是很有用的,而调度函数更新则可以帮助你避免陈旧值的问题。
祝您好运!