setState
背景介绍
最近开始从vue转向react,当然是从最基础(chun)开始一步一步踏实学起。
当使用到setState这个Api碰到了一点有意思的疑惑,顺手记录下来。
查询对应源码内容觉得比较难以理解所以在下方以一个简单Demo记录下setState不同状态下对应实现原理。
记录问题
异步更新原则
当然我们都清楚setState函数是react将对组件的state更改排入队列进行批量更新。注意这里的批量,我们来看一段伪代码:
export default class DemoPage extends React.Component {
public state = {
name: 1,
};
// ES7方式 修改组件this 对比vue太恶心了.. 如果是传参只能使用箭头函数的方式了
private onBtnClick = () => {
this.setState({
name: this.state.name + 2,
});
this.setState({
name: this.state.name + 1,
});
};
render() {
return (
<React.Fragment>
<div>{this.state.name}</div>;
<button onClick={this.onBtnClick}>更新BTN值</button>
</React.Fragment>
);
}
}
当我点击页面上的按钮元素时候。
此时页面上展示this.state.name内容为2,并不是所期待的4。这很好理解,批量更新原则嘛,(react内部会对state的值进行缓存最终合并一次性更新)乍一看和Vue大同小异。
此时我们来看看另一种写法。
使用setTimeout处理
export default class DemoPage extends React.Component {
public state = {
name: 1,
};
// ES7方式 修改组件this 对比vue太恶心了.. 如果是传参只能使用箭头函数的方式了
private onBtnClick = () => {
setTimeout(() => {
this.setState({
name: this.state.name + 2,
});
this.setState({
name: this.state.name + 1,
});
})
};
render() {
return (
<React.Fragment>
<div>{this.state.name}</div>;
<button onClick={this.onBtnClick}>更新BTN值</button>
</React.Fragment>
);
}
}
此时我们使用setTimeout将两次setState包裹起来,嗯。按照vue中的理解,期待的结果应该还是2。
当我天真(zu gou cai)的以为页面上会打出2的时候,发现页面呈现结果是4!!
what! 怎么会这样,按照我的理解,不是说好了批量更新策略,即使在setTimeout之后,下一个队列中应该也是批量呀。这是什么操作,为什么会这样。不行我要翻出来看看!
原理解析
在一通源码(bai du)查阅下,终于搞懂了是个什么东西。为什么会这样。。
我们来看看这段伪代码,非常精简的react关于setState的解析,当然再高深了我也不会,我也写不出来。
// 为了方便阅读 我将相关方法都简化在了这个文件中
let isBatchingUpdate = true; // 默认页面未渲染过,react批量异步更新
function transcation(component) {
// 同步缓存状态
component.state = component.pendingState;
// 渲染页面
component.render();
// 关闭批量更新内容 渲染一次之后 关于state的操作全都是非批量更新
isBatchingUpdate = false;
}
class MyComponent {
constructor() {
this.state = { number: 0 }; // 组件自己拥有的state
this.pendingState = { ...this.state }; // react内部会缓存一份state实例对象
}
// 自己实现这个setState方法
setState(obj) {
// 更新缓存的事例对象
this.pendingState = { ...this.pendingState, ...obj };
if (!isBatchingUpdate) {
console.log('进入')
// 如果页面已经更新过了 那么isBatchingUpdate为false
// 此时就执行非批量更新 每次修改state都会进行更新state的值
transcation(this);
}
}
// 模拟页面更新方法
update() {
setTimeout(() => {
this.setState({ number: this.state.number + 1 });
this.setState({ number: this.state.number + 2 });
this.setState({ number: this.state.number + 3 });
}, 100);
transcation(this);
}
render() {
console.log(this.state.number);
}
}
const cmp = new MyComponent();
// 模拟首次渲染
cmp.update();
下面是一段可以跑起来的伪代码,当然你也可以注释掉setTimeout,测试出来和我们之前的验证结果一模一样。
在react内部其实实现原理也是这样,在第一次页面渲染前(调用过一次render方法之后)关于setState(obj)的写法都是异步缓存更新的。
但是一旦在页面渲染之后,内部pendingState状态改变。此时每次通过setState(obj)更新,每次都会触发单独更新直接更新而不会异步更新。
其实内部并没有多么复杂,就是依赖与pendingState缓存值和isBatchingUpdate判断是否需要批量更新。(好吧其实内部没有异步什么事情,它压根没有micro/macro什么事情呀)。
API总结
此时我们再来看关于setState的官方Api就会通俗很多。
setState(obj)
首先当我们在react内部使用setState(obj)进行调用的时候,如果是第一次render之前,那么所有的修改都会被缓存到pendingState中,之后在render中批量将pengdingState更换为state然后进行渲染。
所以我们每次更改state的值并不能实施获取。
但是刚才也讲过在首次调用render之后,再次调用setState(obj)之后,因为isBatchingUpdate已经打开,所以每次调用setState就会实时修改state的值并且进行页面渲染,此时我们就可以直接获取。
setState(callback)
react官方提供一种setState直接传入一个callback的写法。callback中支持传入一个state参数,这个state每次都会实时的拿到更改后的state,其实就是和我们上文的pendingState是一模一样的。
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
这样一段代码
this.setState((state) => ({ name:state.name+1 }))
this.setState((state) => ({ name:state.name+3 }))
this.setState((state) => ({ name:state.name+2 }))
传入的callback每次拿到的state都是在修改后的新值。
callback 函数中接收的
state和props都保证为最新。callback 的返回值会与state进行浅合并。
注意前两种写法的执行时机都是在组件更新之前进行合并
state并且更新到最新的state中去。
setState(obj[,callback])
react官方提供setState支持传入第二个参数,它会保证在应用更新后(组件更新后执行,compnentDIdUpdate之后)会进行执行。
this.setState({name:11},() => {
console.log('更新完毕')
})
这种方式也会在callback中拿到最新的state,不过需要额外注意的是:
这样的写法类似Vue中的nextTick这个api,它的callback是会在componentDidUpdate之后进行执行。也就是它将在 setState完成合并并重新渲染组件后执行`。 这是和上边两种写法执行实际的不同。
写在结尾
当然我对于react的探索还在继续深入,也许之后在翻回来会发现有一部分的理解很片面。当然也希望大家可以积极指出文章中的不足,共同探讨。