概览
top-down data flow可以说是react在数据管理方面的核心思想, 也是就是react开发者比较熟知的flux, 而提到flux思想, 它的实现就是大名鼎鼎的状态管理框架: redux
个人理解单向数据流动就是一种将数据和修改数据的方法由父组件传递至子组件中去的一种组件组织方式, 子组件再向它的子组件中传递也是这样的, 这是react中的一种组件间的通信方式, 而我们在理解flux的时候可以结合flow, 就是流, 私以为就像瀑布流一样, 无论是从父组件到子组件还是子组件到它的子组件, 都是从上到下的, 就好像我们的瀑布一样
而今天呢, 就和大家聊聊react中原生的单向数据流动, 首先呢, 我们一起来看一看官网的一个例子, 从官网的这个例子中一步步了解react单向数据流动的思想
示例: 温度计算
这里我们写一个计算水在输入的温度下是否会沸腾的程序, 部分代码没有完全按照官网的来, 比如:
- 不在
constructor中初始化state, 一般只在state中有一些值来自props中的时候才会在constructor中初始化state - 不在
constructor中给class的方法绑定this, 而是直接使用箭头函数 - 标识不同类型温度的
props直接由父组件传入 - 最后
状态提升部分的代码和官网不一致, 但结果是一样的
BoilingVerdict:
import React from 'react';
const BoilingVerdict = props => {
const { celsius } = props;
if (celsius >= 100) {
return <p>水会沸腾</p>;
}
return <p>水不会沸腾</p>;
};
export default BoilingVerdict;
Calculator:
import React, { Component } from 'react';
import BoilingVerdict from './BoilingVerdict';
class Calculator extends Component {
state = {
temperature: ''
}
handleChange = e => {
this.setState({
temperature: e.target.value
});
}
render() {
const { temperature } = this.state;
return (
<fieldset>
<legend>输入摄氏温度:</legend>
<input
value={temperature}
onChange={this.handleChange}
/>
<BoilingVerdict
celsius={parseFloat(temperature)}
/>
</fieldset>
);
}
}
export default Calculator;
然后在这个时候, 比如我们有了新的需求:
- 新增华氏温度输入功能
- 同步摄氏温度和华氏温度
这个时候我们写一个叫TemperatureInput的组件, 并且给它一个叫type的props, 用以区分它输入的是摄氏度还是华氏度
TemperatureInput:
import React, { Component } from 'react';
class TemperatureInput extends Component {
state = {
temperature: ''
}
handleChange = e => {
this.setState({
temperature: e.target.value
});
}
render() {
const { temperature } = this.state;
const { type } = this.props;
return (
<fieldset>
<legend>输入{type}:</legend>
<input
value={temperature}
onChange={this.handleChange}
/>
</fieldset>
);
}
}
export default TemperatureInput;
Calculator:
import React, { Component } from 'react';
import TemperatureInput from './TemperatureInput';
class Calculator extends Component {
render() {
return (
<div>
<TemperatureInput type="摄氏温度" />
<TemperatureInput type="华氏温度" />
</div>
);
}
}
export default Calculator;
此时我们有两个输入框了, 但它们依旧是各玩各的, 并没有同步数据, 也就是二者之间没有完成组件间通信这个动作, 同时也没有办法在Calculator中渲染BoilingVerdict, 因为温度值在TemperatureInput组件中, Calculator无法得知, 也就是说, 目前为止, 我们的组件结构如下:
两个TemperatureInput组件中有着自己各自的state, 一个是摄氏温度, 一个是华氏温度, 这个state Calculator不知道, BoilingVerdict也不知道, 那要怎么让它们知道呢, 这就要提到本文的主题: 状态提升了
状态提升
In React, sharing state is accomplished by moving it up to the closest common ancestor of the components that need it. This is called “lifting state up”.
在React中, 将多个组件中需要共享的state向上移动到它们的最近共同父组件中,便可实现共享state. 这就是所谓的“状态提升”
在此例中, 也就是说我们需要将TemperatureInput中的state放到它们最近的共同父组件Calculator中, 根据数据单向流动的定义, 我们也可以这么理解:
- 父组件
Calculator将数据(此例中是温度值)传到TemperatureInput中 - 父组件
Calculator还要将修改数据的方法传到TemperatureInput中
相信到这里, 大家不难看出, 我们接下来是要把TemperatureInput组件改为受控组件, 关于受控组件, 大家可以看看我的这篇文章: react受控/非受控组件
代码如下:
BoilingVerdict保持不变:
import React from 'react';
const BoilingVerdict = props => {
const { celsius } = props;
if (celsius >= 100) {
return <p>水会沸腾</p>;
}
return <p>水不会沸腾</p>;
};
export default BoilingVerdict;
关键在于我们的Calculator组件和TemperatureInput组件, TemperatureInput:
import React, { Component } from 'react';
class TemperatureInput extends Component {
handleChange = e => {
const { onTemperatureChange } = this.props;
onTemperatureChange(e.target.value);
}
render() {
const { type, temperature } = this.props;
return (
<fieldset>
<legend>输入{type}:</legend>
<input
value={temperature}
onChange={this.handleChange}
/>
</fieldset>
);
}
}
export default TemperatureInput;
除了一开始的type之外还需要数据和修改数据的方法, 它们分别是temperature和onTemperatureChange, 就如官网提到的, 值和修改值的方法一般的命名方式为: xxx, onXxxChange, 就比如react约定的input的value和onChange那样
接下来的重点是Calculator组件, 先来看一下它的完整的代码(和官网的有些出入):
import React, { Component } from 'react';
import TemperatureInput from './TemperatureInput';
import BoilingVerdict from './BoilingVerdict';
//华氏温度→摄氏温度
const toCelsius = fahrenheit => {
return (fahrenheit - 32) * 5 / 9;
};
//摄氏温度→华氏温度
const toFahrenheit = celsius => {
return (celsius * 9 / 5) + 32;
};
//转换温度的方法
/**
* @description 输入的温度值无效就返回空串, 反之返回保留三位小数并四舍五入后的转换结果
* @param {温度} temperature
* @param {转换温度的函数} convert
* @returns 空串 || 转换之后的温度
*/
const handleConvertTemperature = (temperature, convert) => {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
};
class Calculator extends Component {
state = {
celsius: '', //摄氏温度
fahrenheit: '' //华氏温度
}
//处理摄氏温度变化的方法
handleCelsiusChange = temperature => {
this.setState({
celsius: temperature,
fahrenheit: handleConvertTemperature(temperature, toFahrenheit)
});
}
//处理华氏温度变化的方法
handleFahrenheitChange = temperature => {
this.setState({
fahrenheit: temperature,
celsius: handleConvertTemperature(temperature, toCelsius)
});
}
render() {
const { celsius, fahrenheit } = this.state;
return (
<div>
<TemperatureInput
type="摄氏温度"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange}
/>
<TemperatureInput
type="华氏温度"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange}
/>
<BoilingVerdict
celsius={parseFloat(celsius)}
/>
</div>
);
}
}
export default Calculator;
首先我们需要两个值, 就是摄氏温度和华氏温度:
state = {
celsius: '', //摄氏温度
fahrenheit: '' //华氏温度
}
以及修改这两个值的方法:
//处理摄氏温度变化的方法
handleCelsiusChange = temperature => {
this.setState({
celsius: temperature,
fahrenheit: handleConvertTemperature(temperature, toFahrenheit)
});
}
//处理华氏温度变化的方法
handleFahrenheitChange = temperature => {
this.setState({
fahrenheit: temperature,
celsius: handleConvertTemperature(temperature, toCelsius)
});
}
由于两个TemperatureInput中, 修改任意一个温度, 另一个温度也要相应的变化, 所以在上面修改这两个值的方法中, 任意一个修改, 也要相应的修改另一个, 但不能盲目的改, 因此需要处理二者之间相互转换的方法:
//华氏温度→摄氏温度
const toCelsius = fahrenheit => {
return (fahrenheit - 32) * 5 / 9;
};
//摄氏温度→华氏温度
const toFahrenheit = celsius => {
return (celsius * 9 / 5) + 32;
};
然后还要有容错的机制, 比如当用户将温度值全部删掉的情况, 所以单有温度转换的方法还不够, 还需要处理这些异常输入值的方法, 同时正常的情况也包含在内, 因此这个方法还要用到上面的两个温度转换的方法:
//转换温度的方法
/**
* @description 输入的温度值无效就返回空串, 反之返回保留三位小数并四舍五入后的转换结果
* @param {温度} temperature
* @param {转换温度的函数} convert
* @returns 空串 || 转换之后的温度
*/
const handleConvertTemperature = (temperature, convert) => {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
};
同时我们还要展示最后的结果, 此时的Calculator知道了温度值, 使得结果的展示成为了可能
这里只用关注摄氏温度即可, 因为上面我们已经将两个温度做了关联了, 其实只关注华氏温度也可以, 但对于我们大家来说, 还是摄氏温度比较熟悉, 因此这里使用摄氏温度:
<BoilingVerdict
celsius={parseFloat(celsius)}
/>
至此, 我们组件的结构如下:
总结
至此, 使用状态提升完美的完成了我们的需求, 这里的代码和官网有一些出入, 个人觉得这样的写法冗余更少, 主要体现在:
- 不必要的
constructor中的初始化和this的绑定当然了, 官网的示例之所以这么写, 个人理解是想兼顾各个层次的人群, 我这里使用了我个人理解的一个更加符合业务场景的方式来书写, 这里书写方式的不同也欢迎大家在评论区一起交流探讨
- 两个温度值之间转换的时机, 我选择在任意一个值
setState修改的时候顺带修改另一个值, 这样更清晰, 更易理解 - 区分温度的
props完整(全)地由父组件传入, 子组件中不再存在关于区分温度的任何代码, 使得子组件成为了受控组件
好, 那么这就是这篇文章的全部内容了, 有任何问题欢迎在评论区探讨, 最后, 如果你觉得这篇文章写得还不错, 别忘了给我点个赞, 如果你觉得对你有帮助, 可以点个收藏, 以备不时之需
参考文献: