setState是同步还是异步

2024/8/1 React
setState(nextState, callback?)
1

setState将组件的state的更改加入队列。它告诉React该组件及其子组件需要使用新的state来重新渲染。调用setState时不会更改已执行代码中当前的state,它只影响从下一个渲染开始返回的this.state

参数

  • nextState:一个对象或者函数。

    • 如果你传递一个对象作为nextState,它将浅层合并到this.state中。
    • 如果你传递一个函数作为nextState,它将被视为更新函数。它必须是个纯函数,应该以已加载的stateprops作为参数,并且应该返回要浅层合并到this.state中的对象。React会将你的更新函数放入队列中并重新渲染你的组件。在下一次渲染期间,React将通过应用队列中的所有的更新函数来计算下一个state
  • 可选的callback:如果你指定该函数,React将在提交更新后调用你提供的callback

注意

  • setState视为请求而不是立即更新组件的命令。当多个组件更新它们的state来响应事件时,React将批量更新它们,并在这次事件结束时将它们一并重新渲染。在极少数情况下,你需要强制同步应用特定的state更新,这时你可以将其包装在flushSync中,但这可能会损害性能。
  • setState不会立即更新this.state。这让在调用setState之后立即读取setState成为了一个潜在的陷阱。相反请使用componentDidUpdate或设置setStatecallback参数,其中任何一个都保证读取state将在state的更新后触发。如果需要根据前一个state来设置state,那么可以传递给nextState一个函数。

在react源码中他是同步的方法,通过队列的形式更新state的值,因此展现给人是异步更新的状态,但实际上它是一个同步的方法。

  • 同步的情况(React17及之前的版本)

    • setTimeout中是同步的。
    • 在原生事件中是同步的,即通过dom绑定事件的方式实现(element.addEventListener())。
  • 异步的情况

    • 在合成事件中是异步的,这里说的异步实际上是react的批量更新,达到了提升性能的目的。
    • 在生命周期中是异步的。

在React17及之前的版本,setStatesetTimeout等异步函数和原生事件内是同步执行的,在18版本之后是批量更新的。React17可以通过ReactDOM.unstable_batchedUpdates来实现批量更新state

执行流程:

  1. 首先将setState中的参数nextState存储到pendingState暂存队列中。

  2. 判断当前React是否处于批量处理状态:

    • 是:则将组件推入待更新队列(dirtyComponents)。
    • 不是:则设置更新批量处理状态为ture,然后再将组件推入待更新队列(批量更新完成后,设置批量处理状态为false,执行事务步骤)。
  3. 调用事务(Transaction)wapper方法遍历组件待更新队列dirtyComponents,执行更新。

  4. componentWillRecevieProps执行。

  5. state暂存队列合并,获取最终要更新的state,队列置空(ps:函数参数也是在这里确定值获取到prevState)。

    Object.assign(nextState, typeof partial === 'function'
    ? partial.call(inst, nextState, props, context)
    : partial);
    
    1
    2
    3
  6. componentShouldUpdate执行,根据返回值判断是否更新。

  7. componentWillUpdate执行。

  8. 执行真正的更新:render

  9. componentDidUpdate执行。

最简单的话来讲就是state接收到一个新状态不会被立即执行,而是会被推入到pendingState队列(Queue)中,随后判断isBatchingUpdates的值,为true,则将新状态保存到dirtyComponents(脏组件)中;为false的话,遍历dirtyComponents,并调用updateComponent方法更新pengdingState中的stateprops,将队列初始化。

React的更新是基于Transaction(事务)的,Transacation就是给目标执行的函数包裹一下,加上前置和后置的hook(有点类似koa的middleware),在开始执行之前先执行initialize hook,结束之后再执行close hook,这样搭配上isBatchingUpdates这样的布尔标志位就可以实现一整个函数调用栈内的多次setState全部入pending队列,结束后统一apply了。 但是setTimeout这样的方法执行是脱离了事务的,react管控不到,所以就没法batch了。

  1. 钩子函数和合成事件中

    在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBatchingUpdatestrue。按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件DidMount后会将isBatchingUpdates设置为false。这时将执行之前累积的setState

  2. 异步函数和原生事件中

    由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBatchingUpdates被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。

如何获取到上一次的state的值?

// ClassComponent
this.setState((prevState, props) => ({
  // ...
}))

// FunctionComponent
const [count, setCount] = useState(0);
setCount(prev => {
  // ...
})
1
2
3
4
5
6
7
8
9
10

参考文章

最近更新: 2024年09月25日 16:42:19