第5章 React状态管理
本章先简要介绍state(状态)、setState()方法、forceUpdate()方法、状态提升等内容,再介绍状态的基础应用开发以及状态提升的应用开发等内容。
5.1 React状态管理概述
5.1.1 state
组件中的 state 包含了随时可能发生变化的数据。state 与 props 类似,但state 由用户自定义,是私有的,并且完全受控于当前组件。它是一个普通 JavaScript 对象。如果某些数据(值)未用于渲染或数据流,就不必将其设置为 state。此类数据可以在组件实例上定义。
如例5-1所示的类组件Clock,在每次组件更新时,render()方法都会被调用,但只要在相同的 DOM 节点中渲染 ,就只有一个Clock 组件实例被创建。
【例5-1】 类组件Clock的代码。
class Clock extends React.Component {
render() {
return (
Hello, world!
It is {this.props.date.toLocaleTimeString()}.
);
}
}
可以通过三个步骤向类组件中添加局部state:把render()方法中的 this.props替换成 this.state;添加一个构造方法,然后在该方法中为this.state赋初值;转移属性(从组件被调用时的props转换到组件内)。将例5-1中的date从props移动到state中,经过转换后的带state的类组件Clock代码如例5-2所示。
【例5-2】 转换后的带state的类组件Clock的代码。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
5.1.2 setState()方法
setState(updater, [callback])方法简称为setState()方法。将对组件state的更改排入队列,并通知React使用更新后的state重新渲染此组件及其子组件。这是用于更新UI界面以响应事件和处理服务器数据的主要方式。
可以在组件的构造方法中给this.state赋值。除了在组件构造方法中直接给this.state赋值之外,应使用setState()方法来修改state,而不要直接修改state(如赋值语句)。同时,可以把多个state的setState()方法调用合并成一个方法。
在调用setState()方法时,React会先把提供的对象合并到当前的 state,再分别调用 setState() 方法来单独地更新它们。
不管是父组件或是子组件,都无法知道某个组件是否有state,并且它们也并不关心这个组件是函数组件还是类组件。组件可以选择把它的 state 作为 props 向下传递到子组件中,但是组件本身无法知道它是来自组件(或父组件)的 state或 props,还是手动输入的。这通常会被叫作自上而下的单向数据流。任何的 state 总是属于特定的组件,而且从该 state 派生的任何数据或 UI 界面只能影响组件树中低于它们的组件(如它们的子组件)。如果把一个以组件树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,它只能向下流动。
在调用setState()方法后,若立即读取this.state,可能会有隐患。为了消除隐患,可以使用componentDidUpdate()方法或者setState()方法,这两种方式都可以保证在应用更新后触发。
除非shouldComponentUpdate()方法返回 false,否则setState()方法将始终执行重新渲染操作。如果使用可变对象,且无法在 shouldComponentUpdate() 方法中实现条件渲染,那么仅在新旧状态不同时调用setState()方法,就可以避免不必要的重新渲染。
5.1.3 forceUpdate()方法
组件的forceUpdate(callback)方法简称为forceUpdate()方法。在默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。如果render()方法依赖于其他数据,就可以调用 forceUpdate() 方法强制让组件重新渲染。
调用forceUpdate()方法将会导致组件调用render()方法,此操作会跳过该组件的 shouldComponentUpdate()方法。但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标签发生变化,React 仍将只更新 DOM。
通常,应避免使用forceUpdate()方法,尽量在render()方法中使用 this.props 和 this.state。
5.1.4 状态提升
当多个组件需要反映相同的变化数据时,可以将共享状态提升到最近的共同父组件中去。这和面向对象设计(如Java开发)时将子类共同内容抽取到父类中的思想类似。在 React 中,将多个组件中需要共享的 state 向上移动到它们最近的共同父组件中,便可实现多个组件共享 state。这就是所谓的“状态提升”。
React中的任何可变数据应当只有一个相对应的唯一数据源。通常,state 都是先添加到需要渲染数据的组件中去。如果其他组件也需要这个 state,那么就可以将它提升至这些组件最近的共同父组件中。
虽然提升 state 方式比双向绑定方式需要编写更多的代码,但带来的好处是,排查和隔离 bug 所需的工作量将会变少。还可以使用自定义逻辑来拒绝或转换用户的输入。
在 UI 界面中发现错误时,可以检查问题组件的 props,并且按照组件树结构逐级向上搜寻,直到定位到负责更新 state 的组件。
5.2 状态的基础应用
5.2.1 开发示例
在firstreact根目录下创建stateexample子目录,在firstreact\stateexample目录下创建文件stateexample.html,代码与例1-3所示的代码相同。在firstreact\stateexample目录下创建文件index.js,代码如例5-3所示。
【例5-3】 在firstreact\stateexample目录下创建的文件index.js的代码。
const divReact = document.getElementById('root');
const infoMap= {
title1info:'状态和生命周期的应用',
nowInfo:'现在时间是:',
endInfo:'。',
title2info:'数据流应用示例1',
title3info: 'setState()方法示例',
countInfo:'现在的计数是:',
helloInfo:'Hello React!',
btn1info:'箭头函数',
btn2info:'bind()方法',
title4info:'数据流应用示例2',
title5info:'数据流应用示例3',
}
class ClockReactComp extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
//生命周期的方法
componentDidMount() {
this.timerId = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerId);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
{infoMap.title1info}
{infoMap.nowInfo}{this.state.date.toLocaleTimeString()}
{infoMap.endInfo}
);
}
}
function FormattedDate(props) {
return {infoMap.nowInfo}{props.date.toLocaleTimeString()}{infoMap.
endInfo}
;
}
class ClockReactComp2 extends React.Component {
static defaultProps = {
propsDate: new Date()
};
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerId = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerId);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
{infoMap.title2info}
);
}
}
class ClockReactComp3 extends React.Component {
static defaultProps = {
propsDate: new Date()
};
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerId = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerId);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
);
}
}
//数据流
function ClockDataFlow() {
return (
);
}
class ClockReactComp4 extends React.Component {
static defaultProps = {
propsDate: new Date()
};
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerId = setInterval(
() => this.tick(),
Math.ceil(Math.random()*9)*1000
);
}
componentWillUnmount() {
clearInterval(this.timerId);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
);
}
}
function ClockDataFlow2() {
return (
);
}
class CountReactComp extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
this.timerId = setInterval(
() => this.count(),
1000
);
}
count() {
this.setState((prevState, props) => ({
count: prevState.count + props.increment
}));
}
componentWillUnmount() {
clearInterval(this.timerId);
}
render() {
return (
{infoMap.title3info}
{infoMap.countInfo}{this.state.count}{infoMap.
endInfo}
);
}
}
//注意代码的顺序
//defaultProps应用
CountReactComp.defaultProps = {
increment: 1
};
class ParamsEventComp extends React.Component {
constructor(props) {
super(props);
this.state = {
name: infoMap.helloInfo
};
}
passParamsClick(name, e) { //事件对象e要放在最后
e.preventDefault();
alert(name);
}
render() {
return (
{/* 通过箭头函数方法传递参数 */}
);
}
}
class ParamsEventComp2 extends React.Component {
constructor(props) {
super(props);
this.state = {
name: infoMap.helloInfo
};
}
passParamsClick(name, e) {
e.preventDefault();
alert(name);
}
render() {
return (
{/* 通过 bind()方法绑定传递参数 */}
);
}
}
const exampleState= (
{infoMap.title4info}
数据流应用示例3
{infoMap.btn1info}
{infoMap.btn2info}
);
ReactDOM.createRoot(divReact).render(exampleState);
5.2.2 运行效果
运行文件stateexample.html,效果如图5-1所示。注意,由于图5-1所示中输出的时间是运行程序的时间,读者在运行该程序时的结果会与图5-1不同,读者多次运行同一程序时的结果也不会相同,只要正确运行即可。单击图5-1所示中的“箭头函数”按钮,弹出的对话框如图5-2所示。单击图5-1中的“bind()方法”按钮,弹出的对话框如图5-3所示。
图5-1 运行文件stateexample.html的效果
图5-2 单击图5-1所示中的“箭头函数”按钮后弹出的对话框
图5-3 单击图5-1所示中的“bind()方法”按钮后弹出的对话框
5.3 状态的提升应用
5.3.1 开发示例
在项目firstreact根目录下创建advancedstateexample子目录,在firstreact\ advancedstateexample目录下创建文件advancedstateexample.html,代码与如例1-3所示的代码相同。在firstreact\advancedstateexample目录下创建文件index.js,代码如例5-4所示。
【例5-4】 在firstreact\advancedstateexample目录下创建的文件index.js的代码。
const divReact = document.getElementById('root');
const infoMap= {
cTempType:'摄氏度',
fTempType:'华氏度',
coolInfo:'今天真冷。',
warmInfo:'今天气温舒适。',
hotInfo:'今天有点热。',
veryHotInfo:'今天真热。',
howInfo:'今天气温如何?',
cCoolStandard: 0,
cWarmStandard: 20,
cHotStandard: 38,
fCoolStandard: 32,
fWarmStandard: 68,
fHotStandard: 100,
outputInfo:'请输入气温(',
outputInfoEnd:'):',
title1info:'状态提升示例',
title2info:'示例1:两个文本框的气温数值相同(不考虑摄氏度和华氏度的区别)',
title3info:'示例2:两个文本框的气温相等(考虑了摄氏度和华氏度之间的转换关系)',
}
const tempType = {
c: infoMap.cTempType,