从零开始的react入门教程(七),react中的状态提升,我们为什么需要使用redux
壹 ❀ 引
在前面的文章中,我们了解到react中的数据由props与State构成,数据就像瀑布中的水自上而下流动,属于单向数据流。而这两者的区别也很简单,对于一个组件而言,如果说props是外部传递进来的属性,那么State便是组件内部自身提供的属性。当然这个组件又可以将自己的State与props作为props继续传递给自己的子级,比如下图:
而对于props与State的通信,我们也在前文中提供了一些例子,但这些例子相对都比较简单,都是容易理解的父传子。但在实际开发中,组件之间的关系往往比我们学习时遇到的例子要复杂的多,高层级组件嵌套,兄弟组件通信,子传父等等都是在写组件时很常见的问题。其实说到这里,我想各位已经想到了redux的状态管理(vue中的vuex)。但本文并不会直接介绍redux,在介绍redux之前,我们还是需要了解react自身提供的状态管理做法,因为只有这样,我们才能明白为什么需要使用redux,以及react的状态提升的局限性在哪。那么本文开始。
贰 ❀ react的状态提升
react中的状态提升其实很好理解,由于react属于单向数据流,当有多个组件需要使用相同的数据时,比如兄弟组件相互感知数据变化,在react中我们一般推荐将这份数据提升到两兄弟的共同父组件中进行管理,这便是所谓的状态提升。
让我们说的更直白点,因为瀑布的水(props)没办法横向流动(兄弟组件之间),所以我们将水源提到两兄弟的顶部,让它同时灌溉这两兄弟,从而满足了瀑布水自上而下的特性。
这里我们直接引用react官方温度计的例子来了解这种做法的含义。
假设现在我们有一个简单的温度计组件,输入一个温度,当大于等于100摄氏度时,文案提示水烧开了,反之提示水未烧开,直接上代码:
function BoilingVerdict(props) {
return props.celsius >= 100 ? <p>水烧开了。</p> : <p>水未烧开。</p>;
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = { temperature: 0 };
}
handleChange(e) {
this.setState({ temperature: e.target.value });
}
render() {
return (
<div className="echo">
<input value={this.state.temperature} onChange={this.handleChange} />
<BoilingVerdict celsius={parseFloat(this.state.temperature)} />
</div>
);
}
}
ReactDOM.render(<Calculator />, document.getElementById('root'));
现在需求升级了,我们知道温度有摄氏度,华氏度不同单位,现在需求是,为用户提供摄氏度与华氏度两个单位的温度输入框,不管用户操作哪一个,另一个温度能自动同步数据展示出对应温度数值。
由于需要两个温度输入框,这里我们直接将温度输入抽离成一个组件,那么完整的代码为:
// 这是判断水温有没有烧开的组件
function BoilingVerdict(props) {
return props.celsius >= 100 ? <p>水烧开了。</p> : <p>水未烧开。</p>;
}
const textType = {
c: '输入摄氏度',
f: '输入华氏度'
};
// 这是抽离的温度组件
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = { temperature: 0 };
}
handleChange(e) {
this.setState({ temperature: e.target.value });
}
render() {
return (
<div>
<p>{textType[this.props.scale]}:</p>
<input value={this.state.temperature} onChange={this.handleChange} />
</div>
);
}
}
// 这是父组件,目前内部只有两个温度组件,并提供了不同的温度单位类型
class Calculator extends React.Component {
render() {
return (
<div className='echo'>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
ReactDOM.render(<Calculator />, document.getElementById('root'));
可以看到,我们抽离了温度输入框组件后,通过不同type,得到了两个互不影响的温度输入框组件实例。
但需求是它们两者不管哪一个输入温度,都应该通过对应的单位换算,同步另一方的温度,并判断当前温度的水有没有烧开,所以我们还需要将这两个组件通过某种方式给联系起来。
OK,我们先准备好摄氏度转换华氏度,与华氏度转为摄氏度的计算方法:
// 这是华氏度转摄氏度
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
// 这是摄氏度转华氏度
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
考虑到输入的值的有效性,官方还提供了一个对于输入值效验的函数:
/**
* 尝试转换温度的方法
* @param {*} temperature 用户输入的温度
* @param {*} convert 用于计算温度的方法,为toCelsius与toFahrenheit其一
*/
function tryConvert(temperature, convert) {
// 将输入的温度转为浮点数,这里的input表示输入,常与output输出一起使用
const input = parseFloat(temperature);
// 判断是不是数字,如果不是数字直接返回0
if (Number.isNaN(input)) {
return 0;
}
// 调用对应的温度计算方法,得到输出的温度
const output = convert(input);
// 保留3位精度的做法
const rounded = Math.round(output * 1000) / 1000;
return rounded;
}
那么到这里,我们已经准备好了温度相互转换的方法,只差状态提升将两个温度输入框组件关联起来了。由于上面的例子中,我们得到了两个温度输入框组件实例,两者都有属于自己的state,相互独立互不影响,前文已经说了状态提升的做法与含义,既然要提升,那自然是要将输入框的state提升到它们共有的且最近的父组件Calculator
中。
通过这种做法,Calculator
组件内部将拥有唯一的数据源(state),而摄氏度与华氏度的温度输入组件将分别与Calculator
进行数据交互,我们要做的就是当一方温度组件进行修改时,需要在父组件中更新state的同时,再利用当前state的数值,去调用对应的单位换算方法得到对应的温度,再传递给对应温度组件即可。
所以说到这里,我们需要对上面的例子进行整体的修改,我们直接上完整的代码,再解释做了什么:
// 这是华氏度转摄氏度
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
// 这是摄氏度转华氏度
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
/**
* 尝试转换温度的方法
* @param {*} temperature 用户输入的温度
* @param {*} convert 用于计算温度的方法,为toCelsius与toFahrenheit其一
*/
function tryConvert(temperature, convert) {
// 将输入的温度转为浮点数,这里的input表示输入,常与output输出一起使用
const input = parseFloat(temperature);
// 判断是不是数字,如果输入不是数字直接返回空,比如啥都没输入的情况
if (Number.isNaN(input)) {
return '';
}
// 调用对应的温度计算方法,得到输出的温度
const output = convert(input);
// 保留3位精度的做法
const rounded = Math.round(output * 1000) / 1000;
return rounded;
}
const textType = {
c: '输入摄氏度',
f: '输入华氏度'
};
function BoilingVerdict(props) {
// 因为不管操作哪一方,都会同步更新摄氏度,所以我们就用摄氏度来判断水烧开没就行了
return props.celsius >= 100 ? <p>水烧开了。</p> : <p>水未烧开。</p>;
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
return (
<div>
<p>{textType[this.props.scale]}:</p>
<input value={this.props.temperature} onChange={this.handleChange} />
</div>
);
}
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = { temperature: '', scale: 'c' };
}
handleCelsiusChange(temperature) {
this.setState({ scale: 'c', temperature });
}
handleFahrenheitChange(temperature) {
this.setState({ scale: 'f', temperature });
}
render() {
// 获取当前操作的温度输入框的单位类型
const scale = this.state.scale;
// 获取最新的温度数值
const temperature = this.state.temperature;
// 获取当前的摄氏度温度
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
// 获取当前的华氏度温度
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div className='echo'>
<TemperatureInput scale="c" temperature={celsius} onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput scale="f" temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
}
ReactDOM.render(<Calculator />, document.getElementById('root'));
代码量貌似有点多,不过没关系,我们来解释下做了什么。
首先,由于状态提升,我们TemperatureInput
所需要的的state提升到了Calculator
中,由Calculator
统一管理,TemperatureInput
不再拥有state,而是接受从Calculator
传递过来的props。
输入框组件只有一个,但事实上我们得到了两个温度输入框实例,无论哪个输入框改变值时,都需要去更新父组件中的state(把当前输入的温度同步过去),所以父组件一定得提供一个更改state的方法给子组件,不然根据props只读性,我们就没办法同步父组件的状态了。
且为了方便区分当前操作的是哪一个温度输入框,所以我们在父组件中分别定义了两个更新state的方法handleCelsiusChange
与handleFahrenheitChange
,他们都会调用setState
更新温度数值,同时记录当前操作的是哪一种温度。
最后,在父组件中,我们根据当前操作的温度类型,用于分别同步计算两个温度,比如输入的是摄氏度100,那么摄氏度自身不用参与计算,只用调用toFahrenheit
得到对应的华氏度即可,反之亦是如此。
比较巧妙的是,由于不管是修改摄氏度或者修改华氏度,我们都能得到一个对应的摄氏度,所以只需要将摄氏度传递给BoilingVerdict
组件,再利用温度判断,即可得到当前水温是否烧开了。
叁 ❀ 为什么需要redux?redux与状态提升的区别
OK,以上就是一个完整的状态提升的例子,它所解决的其实就是兄弟组件共用了一个状态的通信问题。但事实上,一个完整的react应用中需要通信的组件会复杂很多。虽然状态提升也强调了,所谓状态提升,只是将状态提升到离自己最近的父组件上,但实际场景中往往会存在这样的问题:
比如这个例子中,存在交互的兄弟组件是child3,它们最近的父组件还要往上找三层,那这样就造成了一个问题,每次state同步进行传递时,都需要经过child1与child2。那对于这两兄弟就很头疼了,我们明明不需要这个属性,还要作为媒介帮忙传递props,一层两层还好,层级多了传递起来就难以维护了。
我们假设层级传递都不高,其实还会存在另外一个问题,如下图:
在这个例子中,child2与child3有关联,于是状态提升到了parent中,而c4与c5也有关联,它们的状态提升到了child2中,我们可以假象将其关系方法,你会发现state与state之间的关系就像一张复杂的网,我们真的有把握维护好每一个state以及与之关联的state的吗?
与其将状态提升到就近的父组件,能不能直接将所有状态提升到一个最顶点并由它来统一管理呢?那么这个想法就高度契合redux的设计理念了。
我们直接通过两张图来对比两者的区别:
状态提升:
redux统一管理:
所以到这里,即便你之前从未了解过vuex与redux,我想redux是用来做什么的在你心中也应该有一个模糊的雏形了,没错,redux就是一个全局的状态管理器,所有的state都被集中存放在Store中,当某个组件状态被修改,便会通知到Store,然后再决定哪些受影响的组件应该重新渲染。
当然本文并不会立马介绍redux,至于redux如何去使用,应该是下一篇文章应该介绍的事情了。
那么介绍到这里,你是不是又觉得redux强到不行?其实并不是这样,前面我们也说了,redux是用来解决复杂的组件状态通信,如果你的组件状态更新本身就非常简单,仍然使用redux反而多此一举。
肆 ❀ 总
好了,到这里我们介绍了react的状态提升,可以说在未接触redux之前,这就是官方推荐的state管理做法。当然,通过文中的例子,在某些场景下我们也感受到了状态提升的局限性,从而引出了redux的作用,所以下篇文章自然就是介绍redux了,那么到这里,本文结束。