useState的快照机制

发布于 2024-03-03  111 次阅读


首先我们来看一个例子

import React, { useState } from 'react'

export default function () {
  const [val, setVal] = useState(0)
  const handleClick = () => {
    setVal(val + 1)
    setVal(val + 1)
    setVal(val + 1)
  }
  return (
    <div>
      <button onClick={() => handleClick()}>you click me {val} times</button>
    </div>
  )
}

我们给组件设置了一个状态val,当点击按钮时set三次val+1如果我们点击按钮会发生什么呢?

val并不会像我们期望的那样从0变为3而是只会变为1 这是为什么呢?

首先我们先看一下官网的解释:

“正在渲染” 就意味着 React 正在调用你的组件——一个函数。你从该函数返回的 JSX 就像是 UI 的一张及时的快照。它的 props、事件处理函数和内部变量都是 根据当前渲染时的 state 被计算出来的。

与照片或电影画面不同,你返回的 UI “快照”是可交互的。它其中包括类似事件处理函数的逻辑,这些逻辑用于指定如何对输入作出响应。React 随后会更新屏幕来匹配这张快照,并绑定事件处理函数。因此,按下按钮就会触发你 JSX 中的点击事件处理函数。

当 React 重新渲染一个组件时:

  1. React 会再次调用你的函数
  2. 函数会返回新的 JSX 快照
  3. React 会更新界面以匹配返回的快照

作为一个组件的记忆,state 不同于在你的函数返回之后就会消失的普通变量。state 实际上“活”在 React 本身中——就像被摆在一个架子上!——位于你的函数之外。当 React 调用你的组件时,它会为特定的那一次渲染提供一张 state 快照。你的组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值 被计算出来的!

总结来说,当你调用函数触发状态更新时,React会生成当前状态的快照,所有更新状态的操作都会根据这个快照运行。

再看刚才的例子

import React, { useState } from 'react'

export default function () {
  const [val, setVal] = useState(0)
  const handleClick = () => {
    // 执行此函数时 val的快照为0
    setVal(val + 1) // val = 0 + 1 = 1
    setVal(val + 1) // val = 0 + 1 = 1
    setVal(val + 1) // val = 0 + 1 = 1
    // 执行的结果均为1 所以最终结果为1
  }
  return (
    <div>
      <button onClick={() => handleClick()}>you click me {val} times</button>
    </div>
  )
}

上面讲了关于state快照的基本逻辑,那如何避免出现这种预料之外的情况呢?可以使用状态更新函数

还是上面的例子,我们进行如下修改

import React, { useState } from 'react'

export default function () {
  const [val, setVal] = useState(0)
  const handleClick = () => {
    setVal((v) => v + 1)
    setVal((v) => v + 1)
    setVal((v) => v + 1)
  }
  return (
    <div>
      <button onClick={() => handleClick()}>you click me {val} times</button>
    </div>
  )
}

useState提供了更新函数的方式更新状态,这是一种告诉 React “用 state 值做某事”而不是仅仅替换它的方法。

这次再点击按钮时val将会如愿从0变为3

源码解析

我们知道useState有快照更新机制,但是React也提供了状态更新函数,为了理解其中的底层原理我们来看一下React的源码是如何实现的

我们将上面的例子整合一下看一下React内部具体是怎么处理的

function Demo() {
  const [val, setVal] = React.useState(0)
  const handleClick = () => {
    setVal(val + 1)
    setVal(val + 1)
    setVal(val + 1)
    setVal(v => v + 1)
    setVal(v => v + 1)
    setVal(v => v + 1)
  }
  return (
    <div>
      <button onClick={() => handleClick()}>you click me {val} times</button>
    </div>
  )
}

此时我们点击按钮,经过调试发现React内部生成了一个concurrentQueues队列,每次更新状态的操作会在队列里插入四个值

上述六个setVal运行完成后concurrentQueues队列的值为

我们重点关注update,可以看到update中有action,前三次非函数式更新时action的值是静态的,它是根据当前state(val)计算出来的,因为当前state(val)均为0所以计算出来的结果均为1,后三次的update.action传入的是(v) => v+1,后续React会通过调度器将update串联起来

我们再看具体执行更新state的地方

可以看到这里是内部缓存了一个newState变量的它记录就是更新后的状态值,后续通过reducer函数更新

reducer函数

这里可以看到函数内部判断了action的类别如果是函会将回调的结果返回,如果不是函数则将action直接返回,因为这里的参数state是更新后的newState所以当action是状态更新函数时,每次更新的参数就是最新的newState,这也就解释了为什么函数时更新是可以实时获取最新的state的

最后更新于 2024-03-03