读完本文你将知道:
setState 不会立即改变 React 组件中 state 的值 (异步)
setState是异步的,包括在setTimeout里也是异步的 (很多文章说在 setTimeout 里是同步的,我这里用 react18 测试依然是异步)
setState 通过引发一次组件的更新过程来引发重新绘制
- 多次
setState 函数调用产生的效果会合并(批处理)
setState 的特性——批处理
如果在同一周期多次调用 setState ,后调用的 setState 将覆盖先调用的 setState 的值,例如:
1 2 3 4 5
| this.setState({count: state.count + 1}); this.setState({count: state.count + 1}); this.setState({count: state.count + 1});
|
执行 3 次 +1,但最后只加了 1 次;若在 setTimeout 中多次调用,结果也一样
1 2 3 4 5 6 7
| setTimeout(() => { this.setState({count: state.count + 1}); this.setState({count: state.count + 1}); this.setState({count: state.count + 1}); }, 0)
|
因为这样的操作相当于 Object.assign,最后一个会把前面的都给覆盖
1 2 3 4 5 6 7
| Object.assign( state, {count: state.count + 1}, {count: state.count + 1}, {count: state.count + 1}, )
|
同一个时期,多次调用,会合并
函数组件和类组件在同一时期,多次调用setState,会合并。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const DemoState = (props) => { let [number, setNumber] = useState(0);
const add = () => { setNumber(number+1); console.log(number);
setNumber(number+1); console.log(number);
setNumber(number+1); console.log(number); }
return ( <div> <span>{ number }</span> <button onClick={() => { add() }} >点击加 1</button> </div> ) }
|
若上面的 3 次 +1 都放在 setTimeout 执行,也是会合并的,并且仍然是异步 (很多文章说在 setTimeout 里是同步的,我这里用 react18 测试依然是异步)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const DemoState = (props) => { let [number, setNumber] = useState(0);
const add = () => {
setTimeout(() => { setNumber(number+1); console.log(number);
setNumber(number+1); console.log(number);
setNumber(number+1); console.log(number); }, 0) } return (<div> <span>{ number }</span> <button onClick={() => { add() }} >点击加 1</button> </div>) }
|
函数组件在不同时期,会合并
函数组件多次调用 +1 操作,分别在不用时期:一个在 setTimeout 外调用,另一个在 setTimeout 内调用。最后合并了,只调用了 1 次 +1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const DemoState = (props) => { let [number, setNumber] = useState(0);
const add = () => { setNumber(number+1); console.log(number);
setTimeout(() => { setNumber(number+1); console.log(number); }, 0) } return (<div> <span>{ number }</span> <button onClick={() => { add() }} >点击加 1</button> </div>) }
|
类组件在不同时期,不会合并
类组件多次调用 +1 操作,分别在不用时期:一个在 setTimeout 外调用,另一个在 setTimeout 内调用。最后没合并,2 次 +1 都被调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class DemoState2 extends React.Component { constructor(props) { super(props); this.state = { number: 0 } }
add = () => { this.setState({number: this.state.number + 1}); console.log(this.state.number);
setTimeout(() => { this.setState({number: this.state.number + 1}); console.log(this.state.number); }, 0) }
render() { return ( <div> <span>{ this.state.number }</span> <button onClick={this.add} >点击加 1</button> </div> ) } }
|
当前测试 react 版本:18.1.0。
这可能是 react 的一个 bug,看看后面会不会在函数组件和类组件中保持一致。
下面看看由批处理引发的问题:
问题 1:连续使用 setState,为什么不能实时改变
1 2 3 4 5
| state.count = 0; this.setState({count: state.count + 1}); this.setState({count: state.count + 1}); this.setState({count: state.count + 1});
|
因为 this.setState 方法为会进行批处理,后调的 setState 会覆盖统一周期内先调用的 setState 的值,如下所示:
1 2 3 4 5
| state.count = 0; this.setState({count: state.count + 2}); this.setState({count: state.count + 3}); this.setState({count: state.count + 4});
|
问题 2:为什么要 setState,而不是直接 this.state.xx = oo?
setState 不仅仅修改了 this.state 的值,更重要的是它会触发 React 的更新机制,会进行 diff,然后将 patch 部分更新到真实 dom 里
- 如果直接
this.state.xx = oo 的话,state 的值确实会改,但是它不会驱动 React 重渲染,不会触发后续生命周期,如 shouldComponentUpdate、render 等一系列函数的调用。
- 对于
批处理,多次setState 只产生一次重新渲染,将对 Virtual DOM 和 DOM 树操作降到最小,用于提高性能
问题 3:那为什么会出现异步的情况呢?(为什么这么设计?)
因为性能优化。假如每次 setState 都要更新数据,更新过程就要走五个生命周期,走完一轮生命周期再拿 render 函数的结果去做 diff 对比和更新真实 DOM,会很耗时间。所以将每次调用都放一起做一次性处理,能降低对 DOM 的操作,提高应用性能
问题 4:那如何在表现出异步的函数里可以准确拿到更新后的 state 呢?
setState(stateChange[, callback])
setState((state, props) => stateChange[, callback])
1 2 3 4 5 6 7 8 9 10 11 12 13
| onHandleClick() { this.setState( {count: this.state.count + 1,}, () => { console.log("点击之后的回调", this.state.count); } ); }
this.setState(state => { console.log("函数模式", state.count); return { count: state.count + 1 }; });
|