react中如何正确使用setState(附例子)
概述
setState中对于某个state多次修改,只执行一次(最后一次),所以可以将修改放在同一次中
import React, {Component} from 'react';
class Demotest extends Component {
constructor(props) {
super(props);
this.state = {
number: 1
};
}
componentDidMount() {
this.setState({ number: this.state.number + 1 });
}
addNumber(e) {
this.setState({ number: this.state.number + 1 });
this.setState({ number: this.state.number + 1 });
this.setState({ number: this.state.number + 1 });
console.log(this.state.number);
}
render() {
return (
<div>
<div>
<span>当前数字是{this.state.number}</span>
</div>
<br/>
<div>
<button onClick={e => {
this.addNumber(e);
}}>点击添加
</button>
</div>
</div>
);
}
}
export default Demotest;
初始加载后
这时发现页面上显示的是2,控制台输出的却是1,按道理 componentDidMount
里的应该已经成功了,不然不会显示2,那为什么控制台输出的却是1 呢?
由于 setState
是异步的所以,所以同步代码执行结束后才会执行,所以在 console.log('componentDidMount: ', this.state.number);
执行的时候 state
还没有被改变,所以生命周期里的输出还是原来的值。
此时我们点击按钮,触发函数 addNumber
发现,函数里的三次 setState
只生效了一次 ,页面显示的数字变成了3,控制台输出了**2 **(对应上面代码20行),这是因为多次更新被合并,而异步的原因导致只输出了2。官方文档上明确说明,如果希望通过这里的状态更新一下个状态,需要在 setState
中使用函数来取得
addNumber(e) {
this.setState((nextState) => {
console.log(nextState.number);
return { number: nextState.number + 1 };
});
this.setState((nextState) => {
console.log(nextState.number);
return { number: nextState.number + 1 };
});
this.setState((nextState) => {
console.log(nextState.number);
return { number: nextState.number + 1 };
});
}
此时输出的是2、3、4
这里要说明的是当你在 ****willMount**
前设进行 state
的设置不会 render
的触发,而事件和可以 componentDidMount
触发 render
, 而且原生的事件可以优先这个机制
componentDidMount() {
document.body.addEventListener('click', this.updateData, false);
}
updateData = () => {
this.setState({
number: this.state.number + 1
});
console.log('componentDidMount: ', this.state.number);
};
详解setState
上面概述了下setState会出现的'合并',下面引用官网的一段话
setState()
将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式
将setState()
视为_请求_而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。
setState()
并不总是立即更新组件。它会批量推迟更新。这使得在调用setState()
后立即读取this.state
成为了隐患。为了消除隐患,请使用componentDidUpdate
或者setState
的回调函数(setState(updater, callback)
),这两种方式都可以保证在应用更新后触发。如需基于之前的 state 来设置当前的 state,请阅读下述关于参数updater
的内容。
除非shouldComponentUpdate()
返回false
,否则setState()
将始终执行重新渲染操作。如果可变对象被使用,且无法在shouldComponentUpdate()
中实现条件渲染,那么仅在新旧状态不一时调用setState()
可以避免不必要的重新渲染
下面通过几个例子分析setState在实际应用中的运用和注意点
附上一段源码
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');
};
enqueueSetState: function (publicInstance, partialState) {
if (process.env.NODE_ENV !== 'production') {
ReactInstrumentation.debugTool.onSetState();
process.env.NODE_ENV !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : void 0;
}
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
}
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
function enqueueUpdate(component) {
ensureInjected();
// Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (This is called by each top-level update
// function, like setState, forceUpdate, etc.; creation and
// destruction of top-level components is guarded in ReactMount.)
// 是批处理更新, 默认为false
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
setState的第一个参数
// 官网的api ---> 第一个参数可以是对象或者一个函数
setState(updater, [callback])
如果传入的是对象则会浅层合并到新的 state 中,后调用的 setState()
将覆盖同一周期内先调用 setState
的值
Object.assign(
previousState,
{quantity: state.quantity + 1},
{quantity: state.quantity + 1},
...
)
相当于同个属性被合并。
如果第一个参数是函数,形式如下
(state, props) => stateChange
函数中接收的 state
和 props
都保证为最新, 是你的上次setState的状态值。
具体例子
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props)
this.state = {
number: 0
}
}
// 运行一遍,不理解的对照注释看
add () {
// 进入队列 number: 0 + 222 = 222
this.setState({
number: this.state.number + 222
})
// 进入队列 number: 0 + 5 =5
this.setState({
number: this.state.number + 5
})
// 进入队列 number: 5 + 1 = 6
this.setState((state, props) => {
// 此时state是上次setState中的number ==> 5
console.log('one', state);
return {
number: state.number + 1
}
})
// 进入队列 number: 0 + 1 = 1
this.setState({
number: this.state.number + 1
})
// 进入队列: 1 + 2 = 3
this.setState((state, props) => {
// 此时state是上次setState中的number ==> 1
console.log('two', state);
return {
number: state.number + 2
}
})
// 进入队列: 3 + 1 = 4
this.setState((state, props) => {
// 此时state是上次setState中的number ==> 3
console.log('three', state);
return {
number: state.number + 1
}
})
}
render () {
return (
<div>
<button onClick={e =>{this.add()}}>add</button>
{this.state.number}
</div>
);
}
}
export default App;
输出分别为 5 、 1 、 3,最后页面显示的是4。
第二个参数
setState()
的第二个参数为可选的回调函数,它将在setState
完成合并并重新渲染组件后执行。通常,我们建议使用componentDidUpdate()
来代替此方式。
考虑如下场景:
在同个组件中一个具有进度条的页面需要不停的加载请求,只有当某次请求正确返回数据后出现新的内容区域。当数据返回后你将进度条比例设置为100,同时设置数据到state上。
this.ajax() {
this.$post(xxx.com)
.then(res => {
this.setState({
progress: 100,
data: res
})
})
.catch(err => {console.log(err)})
}
设置为100%是为了数据到来后进度条立马拉满,然后在渲染对应的数据内容区域,这样写会出现进度条其实也拉满了,但是视觉效果没出来数据内容区域就出来的情况。所以用到第二个参数
this.ajax() {
this.$post(xxx.com)
.then(res => {
this.setState({
progress: 100,
}, () => {
this.setState({
data: res
})
})
})
.catch(err => {console.log(err)})
}