setState
React为什么要setState
开发中我们并不能直接通过修改state的值来让界面发生更新,因为我们修改了state之后,希望React根据最新的State来重新演染界面,但是这种方式的修改React并不知道数据发生了变化,React并没有实现类似于Vue2中的Object.defineProperty
或者Vue3中的Proxy
的方式来监听数据的变化,我们必须通过setState来告知React数据已经发生了变化。
🧐 疑惑: 在组件中并没有实现setState的方法,为什么可以调用呢?原因很简单,
setState
方法是从Component中继承过来的。
react中setState
的源码:
/**
*
* @param {object|function} partialState Next partial state or function to
* produce next partial state to be merged with current state.
* @param {?function} callback Called after state is updated.
* @final
* @protected
*/
Component.prototype.setState = function (partialState, callback) {
if (
typeof partialState !== 'object' &&
typeof partialState !== 'function' &&
partialState != null
) {
throw new Error(
'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');
};
🎉 小彩蛋 - 同时附上createElement
的源码的362行开始。
部分代码如下:
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// ...
props.children = childArray;
}
⭕️ 填坑:所以在插槽那里,如果传一个参数则children的类型是
object
,如果传多个那么则是数组
类型。
setState的多种用法
其实setState
中第一个参数是对象,源码中使用Object.assign
将其与this.state
中的合并后更改。
基本用法
this.setState({ message: 'hello react' });
传入回调函数
setState
接受两个参数:
- 第二个参数是一个回调函数,这个回调函数会在更新后会执行,格式如下:
setState(partialState, callback)
this.setState((state, props) => {
// 可以编写关于message的处理逻辑
console.log('App: ', state, props);
return {
message: 'hello react'
}
})
这么做的好处:
- 可以在回调函数中编写新的state的逻辑
- 当前的回调函数会将之前的state和props 传递进来,举例如下:
setState是异步
this.setState({ message: 'hello react' });
console.log(this.state.message);
然后观察控制台的打印结果:
可以发现页面上的内容更改了,但是打印的时候还是原来的值。
在更新后立马拿到正确结果
this.setState({ message: 'hello react' }, () => {
console.log('callback:', this.state.message);
});
console.log(this.state.message);
然后观察控制台的打印结果:
setState异步更新
根据上面的例子可见setState
是异步的操作,我们并不能在执行完setState之后立马拿到最新的state的结果,那为什么setState
设计为异步呢?setState
设计为异步其实之前在GitHub上也有很多的讨论,React核心成员(Redux的作者) Dan Abramov也有对应的回复,有兴趣的同学可以参考一下这个Issue中Dan Abramov(昵称为gaearon)的回答。
对其回答做一个简单的总结:
- setState设计为异步,可以显著的提升性能,如果每次调用
setState
都进行一次更新,那么意味着render
函数会被频繁调用,界面重新渲染,这样效率是很低的,最好的办法应该是获取到多个更新,之后进行批量更新;
import React, { Component } from 'react'
export class App extends Component {
constructor() {
super();
this.state = {
message: 'hello world',
count: 0
}
}
changeText() {
this.setState({ message: 'hello react' }, () => {
console.log('callback:', this.state.message);
});
console.log(this.state.message);
}
add() {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}
render() {
const { message, count } = this.state;
return (
<div>
<h1>setState的详细使用</h1>
<h2>{message}</h2>
<button onClick={() => this.changeText()}>更改文本</button>
<p>当前计数:{count}</p>
<button onClick={() => this.add()}>+1</button>
</div>
)
}
}
export default App
点击按钮后,count会变成1,这就是因为setState是异步更新导致的,如果是同步更新的话,那么应该显示3,因为同步更新的话,每次的this.state.count
的值每次都会不一样然后看一下实际效果吧:
如果想要实现点击加3的效果,那么应该使用setState的第二种用法:
this.setState((state) => {
return {
count: state.count + 1 // 1
}
});
this.setState((state) => {
return {
count: state.count + 1 // 2
}
});
this.setState((state) => {
return {
count: state.count + 1 // 3
}
});
因为这样每次更新时传递过来的state
不是this.state
的值,而是上一次更新后的最新值,因此state
的值是最新的,所以每次更新都会在上一次的基础上加1,最终会加3。来看一下效果吧:
- 如果同步更新了state,但是还没有执行
render
函数,那么state和props不能保持同步,state和props不能保持致性,会在开发中产生很多的问题。以下是回答的原话:
This is because, in the model you proposed,
this.state
would be flushed immediately butthis.props
wouldn’t. And we can’t immediately flushthis.props
without re-rendering the parent, which means we would have to give up on batching (which, depending on the case, can degrade the performance very significantly).There are also more subtle cases of how this can break, e.g. if you’re mixing data from
props
(not yet flushed) andstate
(proposed to be flushed immediately) to create a new state: #122 (comment). Refs present the same problem: #122 (comment).
setState一定是异步的吗?
react 18
之前,在setTimeout
中、在原生的DOM事件中、使用this.setState
更新都是同步的,举个例子:
// setTimeout
setTimeout(() => {
this.setState({ message: 'hello react' });
console.log(this.state.message);
}, 0);
// 原生的DOM事件
btn.addEventListener('click', () => {
this.setState({ message: 'hello react' });
console.log(this.state.message);
});
react 18
之前,前在组件生命周期或React合成事件中,setState
是异步的;react 18
之后,默认所有的操作都被放到了批处理中(异步处理),详细信息可以查看Automatic batching for fewer renders in React 18。如果想在react 18
之后同步拿到更新的结果可以使用flushSync
函数,具体使用如下:
import React, { Component } from 'react';
import { flushSync } from "react-dom"; // 要导入该函数
export class App extends Component {
// ...
changeText() {
flushSync(() => {
this.setState({ message: 'hello react' });
})
console.log(this.state.message); // 只能在这里打印
}
render() {
const { message, count } = this.state;
return (
<div>
<h1>setState的详细使用</h1>
<h2>{message}</h2>
<button onClick={() => this.changeText()}>更改文本</button>
<p>当前计数:{count}</p>
<button onClick={() => this.add()}>+1</button>
</div>
)
}
}
export default App
具体的效果如下:
写在最后
如果大家喜欢的话可以收藏本专栏,之后会慢慢更新,然后大家觉得不错可以点个赞或收藏一下 🌟。
博客内的项目源码在react-app
分支,大家可以拷贝下来。