一、 setState的作用
不使用setState
import React, { Component } from 'react'
export default class componentName extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return (
<div>
<div>counter: { this.state.count }</div>
<button onClick={ () => this.increment() }> +1</button>
</div>
)
}
increment() {
this.state.count += 1
}
}
开发中我们并不能直接通过修改state的值来让界面发生更新:
- 因为我们修改了
state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变 化;- React并没有实现类似于
Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;- 我们
必须通过setState来告知React数据已经发生了变化;
使用setState函数后
import React, { Component } from 'react'
export default class componentName extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return (
<div>
<div>counter: { this.state.count }</div>
<button onClick={ () => this.increment() }> +1</button>
</div>
)
}
increment() {
this.setState({
count: this.state.count + 1
})
}
}
上述代码表明,在点击increment按钮后,state中的count自动递增1,且界面也实时发生了刷新
查看源码发现
setState方法是从Component中继承过来的,其是定义在Component的原型上的Component.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); this.updater.enqueueSetState(this, partialState, callback, 'setState'); };可见
setState可以传递2个参数,一个为新的state对象,另一个为callback
二、setState的异步更新
示例
import React, { Component } from 'react'
export default class componentName extends Component {
constructor(props) {
super(props)
this.state = {
message: 'Hello World'
}
}
render() {
return (
<div>
<div>{ this.state.message }</div>
<button onClick={ () => this.changeText() }>change Text</button>
</div>
)
}
changeText() {
this.setState({
message: 'Hello React'
})
console.log(this.state.message)
}
}
在上述结果中可以看到,在虽然界面上的数据发生了改变,变为了
Hello React但是在控制台中的数据依旧是
Hello World,由此证明了在React中setState是异步更新的
2.1 为什么setState是需要异步更新数据
setState设计为异步,可以显著的提升性能;
- 如果每次调用
setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的; 最好的办法应该是获取到多个更新,之后进行批量更新;
-
在一次界面刷新中可能存在多个
setState -
多个更新会统一放入队列中,合并在一起,进行批量操作
-
如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步; state和props不能保持一致性,会在开发中产生很多的问题;
- 因为
setState的执行速度一般是大于render函数的执行速度的 - 在执行
render函数的过程中,render函数需要将JSX转换为ReactElement对象,并且需要通过diff算法来比较不同后再进行界面的刷新,这就导致了render函数的执行速度一般是比setState要慢的 - 如果setState是同步更新的,那么就会导致在某一些时间点上,state中的数据发生了更新,但是render函数没有执行完毕,其中的state也就没有及时更新,如果此时没有更新的这个state数据又正好是另一个组件的props,那么可能导致在某一些时间点上,子组件中的props和父组件中对应的state中的状态是不一致的,这是十分危险的
import React, { Component } from 'react' function Child(props) { return ( <h2>{ props.name }</h2> ) } export default class componentName extends Component { constructor(props) { super(props) this.state = { message: 'Hello World', name: 'Klaus' } } render() { return ( <div> <div>{ this.state.message }</div> <button id='btn'>change Text</button> {/* 如果setState是同步的,那么就会导致 在某些时刻 父组件的name值和子组件在传递进入的name值是不一致的 这无法保证子组件的props和父组件中的对应元素的状态应时刻保持一致 */} <Child name={ this.state.name } /> </div> ) } } - 因为
2.2 如何拿到异步更新的数据
-
在
setState方法中{/* 这里的异步回调函数的功能有点类似于vue中的$nextTick 等待界面完成更新完毕以后在拿取对应的数据 */} this.setState({ message: 'Hello React' }, () => console.log(this.state.message)) -
componentDidUpdate
componentDidUpdate() {
console.log(this.state.message)
}
注意: 执行顺序是优先执行
componentDidUpdate这个callback中内容在执行
setState中的方法
三、 setState在某些情况下是同步更新的
setState是同步更新还是异步更新主要区分如下:
- 在
组件生命周期或React合成事件中,setState是异步; - 在
setTimeout或者原生dom事件中,setState是同步;
定时器
setTimeout(() => {
this.setState({
message: 'Hello React'
})
console.log(this.state.message)
}, 0)
原生DOM事件
componentDidMount() {
document.getElementById('btn').addEventListener('click', () => {
this.setState({
message: 'Hello React'
})
console.log(this.state.message)
})
}
在
React中,每一个类组件对象都会对应一个updater对象
React会自动计算需要执行合并的事件和是异步执行还是同步执行(在React中的是Batch和Sync)随后由
updater对象对state中的状态进行更新操作
四、 setState中的数据合并
原有数据如下:
{
name: 'Klaus',
age: 23
}
有一个新的数据如下:
{
age: 18
}
使用setState合并后的数据如下
{
name: 'Klaus',
age: 18
}
setState中的数据合并使用的是Object.assign方法Object.assign({}, preState, partialState)所以其对新老state中的数据合并时候,只会覆盖同名的数据类型,且是浅拷贝
五、SetState的自身合并
在点击按钮的时候,进行三次递增操作
increment() {
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
}
可以看出,虽然点击按钮的时候,在代码中对state中的count递增了3次,
但是在界面中, 其实际只递增了1次
原因是 setState是
异步批处理操作的所以setState将3次递增操作合并在一起执行,又setState的合并操作是使用
Object.assign方法来执行合并的,/* 第一次 Object.assign({}, {count: 0}, { count: 1 }) ---> { count: 1 } 第二和第三次 Object.assign({}, {count: 1}, { count: 1 }) ---> { count: 1 } */
但是我们希望其实现递增,而不是直接合并,那么我们可以在使用setState的时候,给其第一个参数传递一个function
increment() {
this.setState((preState, nextProp) => ({
count: preState.count + 1
}))
this.setState((preState, nextProp) => ({
count: preState.count + 1
}))
this.setState((preState, nextProp) => ({
count: preState.count + 1
}))
}
在
setState这个方法中传入callback的时候,callback可以传入上一个的state值和props(nextProps 一般不使用)其执行原理和不使用函数作为参数时候的执行原理是一致的,只不过使用函数作为参数的时候,可以获取上一次的state状态
随后可以通过在上一次状态基础上递增(也就是累加),从而达到批量操作的效果
/* 第一次 Object.assign({}, {count: 0}, { count: 1 }) ---> { count: 1 } 第二次 Object.assign({}, {count: 1}, { count: 2 }) ---> { count: 2 } 第三次 Object.assign({}, {count: 2}, { count: 3 }) ---> { count: 3 } */
补充
react中的合成事件对象
-
因为react可以依据不同的reactDom运行于不同的平台上,所以其在不同的平台上的事件对象都是不同的,
-
在浏览器端,传入的是原生的事件对象(DOM事件对象)
-
在native端,传入的就是原生的控件对象
-
react获取了这些对象,会根据不同的平台特性对事件和事件对象进行了封装,以便于统一操作和使用
-
这样的事件被叫做
合成事件,这样的对象别叫做合成事件对象
上一篇 React组件化(下) 下一篇 React更新流程