为什么在 React 中不推荐使用 setCount(count+1) 这种写法

1,576 阅读4分钟

这是我们的一个基础组件:

function App() {
  const [count, setCount] = useState(0)

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button type="button" onClick={handleClick}>
      count is: {count}
    </button>
  )
}

当我们点击按钮的时候, count 的数量会加 1。如果我们的 handleClick 只有一条语句,确实不会有问题,但是如果我们的 handleClick 是如下的格式呢:

 function handleClick() {
    setCount(count+1);
    setCount(count+2);
 }

结果可能是出人意料的,如果当前我们 count 的值是 0,点击一次按钮,我们期望它的值变成 3,但是事实上它变成了 2。

对于上述问题的解决方案就是使用回调函数:

 function handleClick() {
    setCount((count) => count+1);
    setCount((count) => count+2);
 }

到底是什么原因导致了二者的差异呢?

我们略过所有 usetState 更新的流程,只看结果,最后它会生成一个更新队列来存放要更新的 state 的值,而导致这个差异的原因就出现在这里。

前者生成的更新队列长这样:

image.png

生成更新队列的时候,还不会到更新流程,count 值还都没有来得及更新,所以都是取的同一个。如果 count 的初始值是 0 的话,那么这个队列的值就是固定死的 1 -> 2。

而后者长这样:

image.png

这是两个函数,具体的值不是固定死的了,要后面计算。

生成完更新队列,我们要遍历这个更新队列,计算最终的 count 结果,大概会是这样一段伪代码:

let newState; // 这个将会是最终的 state

// 这个是我们的更新队列,用 next 指针串起来
// 比如它可能是 1 -> 2 -> 3 ....
// 也可能是 (count) => count + 1 -> (count) => count + 2
let queue = 一个队列; 
let head = queue;
do {
  if (typeof head === 'function') {
    newState = head(newState); // 关键点,看
  } else {
    newState = head;
  }
  head = head.next;
} while (head.next != null);

最关键就是根据「是不是函数」区别计算。以至于使用函数的方式,我们可以在还没有真正走完 React 更新流程的时候,就取到了最新的 count 值。

就是因为这段代码,才让我们使用回调函数解决了这个问题。

聪明的你可能还会想呀,出现问题的原因是因为两个临近的 setCount 在同一个更新队列里面,如果想避免这种情况,那就让第二个和第一个不在同一个更新队列里就好了呀,我只要在第一个更新结束后再更新就行了。于是乎你想用一个异步任务包裹它,比如 setTimeout

function handleClick() {
    setCount(count + 1);
    setTimeout(() => {
      setCount(count + 2);
    }, 1000)
}

现实很残酷,还是不行。

这个时候他们确实不在同一队列里面了。分析来看,第一个 setCount 完了,过了 1 秒,它才会执行第二条 setCount 语句。

但是就算我们用 setTimeout 包裹一层 setCount 语句,但是由于 JavaScript 的闭包机制,setTimeout 里面的 count 始终指向的是未更新前的,也就是说,无论如何,无论我们的定时器延时多久,都是和 setCount(count+1) 里面用的 count 是一个。(这里我可能没讲清楚😂,想理解这里可能需要更多的文字,并且需要更理解 useEffect 一下,推荐阅读 这篇文章

延时再久,也没有用。这一点算是函数式组件特殊的地方。

如果用 Class 组件,这么写就可以奏效了(注意,是奏效,但是不推荐)。

handleClick() {
    this.setState({
      count: this.state.count + 1
    })

    setTimeout(() => {
      this.setState({
        count: this.state.count + 2
      })
    })
 }

就算 Class 组件更新了,它引用的 State 始终是同一份引用。并且由于 setTimeout 在没更新前不会去执行,所以当真正读 this.state.count 的时候就是新的了。

尽管如此,为了避免难以察觉的错误,比如在 Concurrent Mode 中可能就有问题,总之,我们也不推荐使用这种写法,还是希望能在任何读 state 的时候采用回调的形式:

 this.setState((prevState) => {
      count: prevState.count + 1
})

同样的,当我们单个使用 setCount(count + 1) 这种写法是可以的,但是为了避免意想不到的问题,我们是不推荐使用这种写法的,一般来说,我们也会给 Lint 工具配置这种限制,让写代码的人不这么写 :)

希望这篇能解答你的疑惑,如果有觉得我讲得不好的地方,讲得不明白的地方,想看我写的东西!告诉我呀,最后,撒花撒花吧。

早上土豪了一把,点了一份鸡蛋肠粉,好吃好吃。