Skip to content

setState

前言

虽然现在有了hooks,但是setState还是一个非常重要的知识点,在没有hooks 的时代,有状态的组件一定会用到这个方法,所以这个方法还是需要掌握的。一般这个问题其实在面试中也比较常见,算是对 react 的熟悉成度做一个考察。一般会问到setState 的第二个参数,以及setState的异步更新机制。这里就来看看这个 api

setState 的更新机制

一般问到这个问题,面试官想要考察的是你对 react 的理解程度,以及你是否了解 react 的更新机制。这里就来看看 react 的更新机制。一般会涉及到批处理机制 batchUpdate ,以及setState的异步更新机制。但是也有可能会问到setState的同步更新机制,以及如何出现同步更新的情况。简单说下这两个问题。 在前面的 react 发展过程中知道 react 的架构是发生了变化,在这个变化中也诞生了三种模式 legacy、blocking、concurrent。一般情况下使用的是 legacy 模式,在 react17 到 react18 的过程中出现了 blocking 模式。可以理解为是一个过渡模式,因为后面的 concurrent 模式是最终想要的。因为模式的不同,state 的更新逻辑也是不同的,这里以 legacy 模式来看 setState 的更新逻辑。

setState 的用法

在 react 的 ui 中的更新会随着 state 的变化而更新。而在类组件中 setState 是更新 state 的方式。

tsx
setState(obj, callback)
  • 第一个参数:当参数是一个对象时,会将这个对象合并到 state 中,如果是一个函数时,会将这个函数的返回值合并到 state 中。而且当前组件的 state 和 props 会作为函数的参数传入。
  • 第二个参数:当 state 更新完成后会执行这个回调函数。可以获取到最新的 state 和 props。

例子:

tsx
// 对象类型
this.setState({ count: 1 }, () => {
  console.log(this.state.count)
})

// 函数类型
this.setState(
  (state, props) => {
    return { count: state.count + 1 }
  },
  () => {
    console.log(this.state.count)
  }
)

如果触发一次 setState react 的底层原理上发生了什么呢?这里就来看看 react 的更新机制。

  • setState 会产生当前更新的优先级,在老版本的 react 中使用的是 expirationTime,在新版本中使用的是 lanes。这个优先级会决定这次更新的优先级,优先级高的更新会先执行。
  • 在 react 的 fiber 架构下的更新机制是从 fiber Root 根部 fiber 向下调和子节点,调和阶段会去对比发生更新的地方,对比优先级(expirationTime),找到更新的的组件,然后触发 render 函数,更新 ui 视图,完成 render 更新。
  • 在调和阶段完成后会进入 commit 阶段,这个阶段会将更新的内容同步到 dom 中,完成 dom 的更新。
  • 如果还在 commit 阶段,会执行 setState 中的回调函数。完成一次 setState 的全过程更新。

在更新阶段有一个优化手段,那就是可以限制这次要不要更新。

  • pureComponent 可以自动对 state 和 props 进行浅比较,如果没有发生变化,那么组件不更新。
  • shouldComponentUpdate 生命周期可以通过判断前后 state 的变化来决定组件需不需要更新,需要更新返回 true,不需要更新返回 false。

setState 的更新原理

类组件在初始化过程中绑定了负责更新的 Updater 对象,在 setState 过程中会调用这个对象的 enqueueSetState 方法,这个方法会将 state 的更新放入到一个队列中,然后在合适的时机去执行这个队列中的更新。这个队列就是更新队列,这个队列中的更新会在合适的时机去执行。这个时机就是在调和阶段,调和阶段会去执行这个队列中的更新,然后更新 ui 视图。

enqueueSetState 主要做了什么

先来看下这个方法的源码

ts
enqueueSetState()
{
  /* 每一次调用`setState`,react 都会创建一个 update 里面保存了 */
  const update = createUpdate(expirationTime, suspenseConfig)
  /* callback 可以理解为 setState 回调函数,第二个参数 */
  callback && (update.callback = callback)
  /* enqueueUpdate 把当前的update 传入当前fiber,待更新队列中 */
  enqueueUpdate(fiber, update)
  /* 开始调度更新 */
  scheduleUpdateOnFiber(fiber, expirationTime)
}

这个方法主要做了三件事情:

  • 创建一个 update 对象,这个对象中保存了更新的内容,以及更新的优先级。
  • 将这个 update 对象放入到 fiber 的更新队列中。
  • 调度更新,触发更新。

一般来说 state 的变化,页面 ui 的更新都是用户事件来触发的。然而 react 的事件采用的是合成事件,所以 state 的批量更新和事件系统有关系。

ts
/* 在`legacy`模式下,所有的事件都将经过此函数同一处理 */
function dispatchEventForLegacyPluginEventSystem() {
  // handleTopLevel 事件处理函数
  batchedEventUpdates(handleTopLevel, bookKeeping)
}
ts
function batchedEventUpdates(fn, a) {
  /* 开启批量更新  */
  isBatchingEventUpdates = true
  try {
    /* 这里执行了的事件处理函数, 比如在一次点击事件中触发setState,那么它将在这个函数内执行 */
    return batchedEventUpdatesImpl(fn, a, b)
  } finally {
    /* try 里面 return 不会影响 finally 执行  */
    /* 完成一次事件,批量更新  */
    isBatchingEventUpdates = false
  }
}

从源码可以看出,是否开启批量更新的关键是 isBatchingEventUpdates 是否为 true。在 react 中如果一个方法中一次更新多次 state 其实最后只会更新一次。执行的流程如图所示:

但是面试过都知道,其实这个规则也会被打破,比如在定时器中更改 state,那函数的执行顺序就会变成这样:

如果在定时器中被打破的批量更新,我们还行让其批量更新的话应该如何做呢?这里就需要用到unstable_batchedUpdates这个 api 了。

ts
setTimeout(() => {
  unstable_batchedUpdates(() => {
    this.setState({ number: this.state.number + 1 })
    console.log(this.state.number)
    this.setState({ number: this.state.number + 1 })
    console.log(this.state.number)
    this.setState({ number: this.state.number + 1 })
    console.log(this.state.number)
  })
})

还有 flushSync 这个 api 能够提升 setState 的优先级,更新的优先级顺序是:

flushSync 中的 setState > 正常执行上下文中 setState > setTimeout ,Promise 中的 setState。

函数组件中的 state

因为 hooks 的加持,函数组件也可以使用 state 了,使用 useState 这个 hook 就可以了。先看下基本使用

tsx
const [state, setState] = useState(initData)
  • state:当前 state 的值
  • setState:更新 state 的方法
  • initData:初始化 state 的值(可以为值,也可以为函数)

如果为值就直接赋值给 state,作为下一次渲染使用。如果为函数,那么会将这个函数的返回值作为 state 的值,作为下一次渲染使用。并且函数中参数 state 是上一次最新的 state 值。

类组件中的 setState 和函数组件中的 useState 有什么异同

相同点

首先从原理角度出发,setState 和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。

不同点

在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。但是 useState 中的 dispatchAction 会默认比较两次 state 是否相同,然后决定是否更新组件。

setState 有专门监听 state 变化的回调函数 callback,可以获取最新 state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。

setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值

小结

上面简单对 setState 的使用方式和原理做了说明,也从源码角度分析了 setState 的更新机制,也对函数的 useState 做了简单的说明和对比,希望对你有帮助

如有转载或 CV 的请标注本站原文地址