setState(nextState, callback?)
setState将组件的state的更改加入队列。它告诉React该组件及其子组件需要使用新的state来重新渲染。调用setState时不会更改已执行代码中当前的state,它只影响从下一个渲染开始返回的this.state。
参数
nextState:一个对象或者函数。- 如果你传递一个对象作为
nextState,它将浅层合并到this.state中。 - 如果你传递一个函数作为
nextState,它将被视为更新函数。它必须是个纯函数,应该以已加载的state和props作为参数,并且应该返回要浅层合并到this.state中的对象。React会将你的更新函数放入队列中并重新渲染你的组件。在下一次渲染期间,React将通过应用队列中的所有的更新函数来计算下一个state。
- 如果你传递一个对象作为
可选的
callback:如果你指定该函数,React将在提交更新后调用你提供的callback。
注意
- 将
setState视为请求而不是立即更新组件的命令。当多个组件更新它们的state来响应事件时,React将批量更新它们,并在这次事件结束时将它们一并重新渲染。在极少数情况下,你需要强制同步应用特定的state更新,这时你可以将其包装在flushSync中,但这可能会损害性能。 setState不会立即更新this.state。这让在调用setState之后立即读取setState成为了一个潜在的陷阱。相反请使用componentDidUpdate或设置setState的callback参数,其中任何一个都保证读取state将在state的更新后触发。如果需要根据前一个state来设置state,那么可以传递给nextState一个函数。
在react源码中他是同步的方法,通过队列的形式更新state的值,因此展现给人是异步更新的状态,但实际上它是一个同步的方法。
同步的情况(React17及之前的版本)
- 在
setTimeout中是同步的。 - 在原生事件中是同步的,即通过dom绑定事件的方式实现(
element.addEventListener())。
- 在
异步的情况
- 在合成事件中是异步的,这里说的异步实际上是react的批量更新,达到了提升性能的目的。
- 在生命周期中是异步的。
在React17及之前的版本,setState在setTimeout等异步函数和原生事件内是同步执行的,在18版本之后是批量更新的。React17可以通过ReactDOM.unstable_batchedUpdates来实现批量更新state。
执行流程:
首先将
setState中的参数nextState存储到pendingState暂存队列中。判断当前React是否处于批量处理状态:
- 是:则将组件推入待更新队列(
dirtyComponents)。 - 不是:则设置更新批量处理状态为
ture,然后再将组件推入待更新队列(批量更新完成后,设置批量处理状态为false,执行事务步骤)。
- 是:则将组件推入待更新队列(
调用事务(Transaction)
wapper方法遍历组件待更新队列dirtyComponents,执行更新。componentWillRecevieProps执行。state暂存队列合并,获取最终要更新的state,队列置空(ps:函数参数也是在这里确定值获取到prevState)。Object.assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);1
2
3componentShouldUpdate执行,根据返回值判断是否更新。componentWillUpdate执行。执行真正的更新:
render。componentDidUpdate执行。
最简单的话来讲就是state接收到一个新状态不会被立即执行,而是会被推入到pendingState队列(Queue)中,随后判断isBatchingUpdates的值,为true,则将新状态保存到dirtyComponents(脏组件)中;为false的话,遍历dirtyComponents,并调用updateComponent方法更新pengdingState中的state或props,将队列初始化。
React的更新是基于Transaction(事务)的,Transacation就是给目标执行的函数包裹一下,加上前置和后置的hook(有点类似koa的middleware),在开始执行之前先执行initialize hook,结束之后再执行close hook,这样搭配上isBatchingUpdates这样的布尔标志位就可以实现一整个函数调用栈内的多次setState全部入pending队列,结束后统一apply了。
但是setTimeout这样的方法执行是脱离了事务的,react管控不到,所以就没法batch了。
钩子函数和合成事件中
在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时
isBatchingUpdates为true。按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件DidMount后会将isBatchingUpdates设置为false。这时将执行之前累积的setState。异步函数和原生事件中
由执行机制看,
setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBatchingUpdates被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。
如何获取到上一次的state的值?
// ClassComponent
this.setState((prevState, props) => ({
// ...
}))
// FunctionComponent
const [count, setCount] = useState(0);
setCount(prev => {
// ...
})
2
3
4
5
6
7
8
9
10
参考文章